opentelemetry笔记

span

https://github.com/open-telemetry/opentelemetry-cpp/blob/f987c9c094f276336569eeea85f17e361de5e518/sdk/src/trace/span.h

在 OpenTelemetry C++ 的 sdk/src/trace 目录中,不同的 span 定义和实现是为了支持追踪(Tracing)功能的多样化需求。以下是一些关键点来帮助您理解它们的区别:

Span 的核心概念

Span 是 OpenTelemetry 中的核心追踪单元,表示一次独立的操作或事件。

每个 Span 包含以下信息:

  • 操作的名称
  • 上下文信息(如 TraceId 和 SpanId)
  • 开始和结束时间
  • 属性(Attributes)和事件(Events)
  • 链接(Links)

在不同文件中的 Span 定义

在 OpenTelemetry 的代码中,Span 有不同的实现和变体,用于适应多种场景:

API 层的 Span

定义在 api/include/opentelemetry/trace/span.h。

这是一个抽象类,定义了 Span 的基本接口,如 SetAttribute、AddEvent、End 等方法。

它提供了一个标准化的接口,供所有 Span 实现遵循。

SDK 层的 Span

定义在 sdk/src/trace/span.h 和实现中(如 span.cc)。

这是 API 层 Span 的具体实现,处理真正的追踪逻辑。

它负责:

记录和存储追踪数据。

与 Tracer 和 SpanProcessor 协同工作,将数据导出到后端。

默认的空操作 Span(DefaultSpan)

定义在 api/include/opentelemetry/trace/default_span.h。

用于在没有实际追踪上下文的情况下,提供一个占位的 Span 实现。

它不会记录任何数据,仅用于传播上下文。

设计背后的原因

抽象与实现分离:

API 层的 Span 是一个接口,定义了所有 Span 的通用行为。

SDK 层的 Span 是具体实现,负责处理数据。

灵活性与扩展性:

不同的 Span 实现可以适应不同的需求,比如实际追踪、空操作实现等。

性能优化:

默认的空操作 Span 减少了不必要的开销。

API 与SDK

API 层的含义

API(Application Programming Interface)层 是为开发者提供的接口,定义了功能的规范和使用方法:

  1. 作用

    • 提供统一的接口,供用户或其他模块调用。
    • 隐藏实现的细节,只暴露功能定义。
    • 例如,opentelemetry::trace::Span 是一个抽象接口,定义了追踪单元的核心操作(如 SetAttributeAddEvent 等)。
  2. 特点

    • 抽象化:API 层主要包含接口和定义,而不涉及具体实现。
    • 独立性:可以独立于具体实现运行。
    • 灵活性:允许不同的实现(如 SDK 层)以自己的方式实现这些接口。
  3. 示例

    在 OpenTelemetry 中,API 层定义了所有核心行为,例如 TracerSpanSpanContext,目的是为用户提供一个统一的追踪接口。


SDK 的含义

SDK(Software Development Kit) 层是 API 的具体实现,负责实现 API 中定义的功能:

  1. 作用

    • 提供 API 的具体实现,包括逻辑、数据存储、导出等。
    • 负责和后端服务(如 OpenTelemetry Collector)交互。
    • 例如,sdk::trace::Spanopentelemetry::trace::Span 的实现类,用于记录和管理追踪数据。
  2. 特点

    • 实现性:SDK 层实现了 API 的所有功能,提供具体的逻辑和行为。
    • 可扩展性:允许用户配置不同的导出器、采样器等组件。
    • 面向后端:将跟踪数据导出到后端进行存储和分析。
  3. 示例

    在 OpenTelemetry 中,sdk::trace::Tracersdk::trace::Span 是 API 层 TracerSpan 的具体实现。SDK 层实现了追踪数据的记录、处理和导出。


API 层与 SDK 层的关系

  • API 层 定义了功能和行为,是用户与库之间的接口。
  • SDK 层 实现了 API 的功能,负责具体逻辑和与后端的交互。
  • 调用关系:用户通过 API 层调用功能,而这些功能的具体实现由 SDK 层提供。

类比

可以将 API 层和 SDK 层的关系比作一个家电:

  • API 层:家电的遥控器,定义了开关、调节等功能。
  • SDK 层:家电的内部组件,具体实现了遥控器的功能(如启动电机、调节温度等)。

Recordable

Recordable 是 OpenTelemetry C++ SDK 中的一个核心接口,用于表示可记录的数据结构,如日志记录或追踪(Tracing)数据。这是一个抽象类,具体实现取决于导出器(Exporter)或处理器(Processor)的需求。以下是关键点:


1. Recordable 的定义

  • 头文件位置sdk/include/opentelemetry/sdk/trace/recordable.h

  • 主要功能

    • 提供一个标准化的数据结构,用于存储追踪或日志记录的数据。
    • 定义了一些通用方法,如设置属性、添加事件和链接等。
  • 代码片段

    cpp 复制代码
    class Recordable
    {
    public:
      virtual ~Recordable() = default;
    
      virtual void SetIdentity(const opentelemetry::trace::SpanContext &span_context,
                               opentelemetry::trace::SpanId parent_span_id) noexcept = 0;
    
      virtual void SetAttribute(nostd::string_view key,
                                const opentelemetry::common::AttributeValue &value) noexcept = 0;
    
      virtual void AddEvent(nostd::string_view name,
                            opentelemetry::common::SystemTimestamp timestamp,
                            const opentelemetry::common::KeyValueIterable &attributes) noexcept = 0;
    
      virtual void AddLink(const opentelemetry::trace::SpanContext &span_context,
                           const opentelemetry::common::KeyValueIterable &attributes) noexcept = 0;
    
      virtual void SetName(nostd::string_view name) noexcept = 0;
    
      virtual void SetTraceFlags(opentelemetry::trace::TraceFlags flags) noexcept = 0;
    
      virtual void SetStartTime(opentelemetry::common::SystemTimestamp start_time) noexcept = 0;
    
      virtual void SetDuration(std::chrono::nanoseconds duration) noexcept = 0;
    };

2. Recordable 的主要方法

  • SetIdentity
    设置 Recordable 的标识,包括 SpanContext 和父级 SpanId
  • SetAttribute
    设置 Recordable 的属性(键值对)。
  • AddEvent
    添加事件(包括名称、时间戳和属性)。
  • AddLink
    添加链接到其他 Span
  • SetName
    设置 Recordable 的名称。
  • SetTraceFlags
    设置追踪标志。
  • SetStartTimeSetDuration
    设置 Recordable 的开始时间和持续时间。

3. 具体实现

  • Recordable 是一个抽象类,具体功能由不同的导出器实现,例如:

4. SpanProcessor 的关系

  • RecordableSpanProcessor 的核心工具,用于将数据从内存中提取并格式化为导出器可用的形式。
  • 例如:
    • SpanProcessor::MakeRecordable 创建 Recordable 的实例。
    • SpanProcessor::OnStartSpanProcessor::OnEnd 使用 Recordable 来处理 Span 数据。

5. 总结

Recordable 是 OpenTelemetry 的一个核心抽象,用于在追踪和日志记录数据的生命周期中充当一个可记录的数据存储单元。它的具体实现由导出器(如 Zipkin 或 OTLP)决定,提供了灵活性以支持各种后端服务。如果需要进一步理解某个导出器的实现,可以查看它的具体代码文件。

项目架构

https://opentelemetry.opendocs.io/docs/specs/otel/overview/

OpenTelemetry C++ 项目是一个实现分布式追踪和指标采集的客户端库,支持多种后端系统。它的架构分为以下几个关键组件或层次,每个组件负责特定的功能:


1. API 层

API 层是开发者直接与 OpenTelemetry 交互的接口。它定义了一组抽象类和接口,允许开发者记录追踪(Tracing)数据和指标(Metrics),而无需了解底层实现。

  • 关键文件
    • opentelemetry/trace/span.h:定义了 Span 类,用于描述分布式追踪中的操作。
    • opentelemetry/trace/tracer.h:定义了 Tracer 类,负责创建和管理 Span
    • opentelemetry/metrics:用于指标的记录和管理。
  • 职责
    • 提供开发者调用的接口,例如创建 Span、设置属性、记录事件。
    • 保持实现的独立性,允许使用不同的 SDK 或后端。

2. SDK 层

SDK 层是 API 的具体实现,负责将追踪数据和指标从内存中提取、处理,并通过导出器(Exporter)发送到后端。

  • 关键文件
    • sdk/trace/span.h:实现了 Span 的具体功能。
    • sdk/trace/processor.h:定义 SpanProcessor,用于处理和批处理 Span 数据。
    • sdk/metrics:实现指标的采集和管理。
  • 职责
    • 提供 TracerSpan 的实现。
    • 管理配置(如采样器、处理器、导出器)。
    • 将数据批量发送给后端。

3. 导出器(Exporter)

导出器负责将追踪数据和指标发送到后端系统,例如 Zipkin、Jaeger、Prometheus 或 OpenTelemetry Collector。

  • 关键文件
    • exporters/zipkin:实现 Zipkin 的导出功能。
    • exporters/otlp:实现 OpenTelemetry Protocol (OTLP) 的导出功能。
  • 职责
    • Recordable(可记录的数据结构)转换为后端支持的格式(如 JSON 或 Protobuf)。
    • 通过 HTTP、gRPC 或文件等方式发送数据。

4. 工具与兼容性层

  • nostd(Non-Standard Library)
    • 提供了一些轻量级的工具类,例如 nostd::span,用于兼容没有 C++17 支持的环境。
  • common
    • 定义了一些通用的工具和数据结构,如属性值(AttributeValue)和时间戳(Timestamp)。

5. 测试与示例

  • 测试
    • 提供了单元测试和集成测试,确保每个模块的功能正常。
    • 测试代码存放在 api/testsdk/test 目录中。
  • 示例
    • 提供了各种使用场景的示例,帮助开发者快速上手。
    • 示例代码存放在 examples 目录中。

6. 项目目录结构

以下是项目目录结构的简要说明:

  • api:API 层定义的接口和抽象类。
  • sdk:SDK 的具体实现,包括追踪和指标的处理。
  • exporters:支持的导出器模块(如 OTLP、Zipkin)。
  • examples:示例代码,演示如何使用 OpenTelemetry C++。
  • common:通用工具和数据结构。
  • nostd:非标准库的实现,用于兼容不同的 C++ 标准。
  • tests:测试代码,验证项目的正确性。

7. 架构工作流程

  1. 开发者调用 API
    • 使用 Tracer 创建 Span,并记录操作信息。
  2. SDK 处理数据
    • SpanProcessor 处理和存储 Span 数据。
  3. 导出器发送数据
    • 数据通过导出器发送到后端系统。
  4. 后端分析
    • 后端系统(如 Zipkin、Jaeger)接收并分析追踪数据。

总结

OpenTelemetry C++ 项目通过模块化设计,将接口、实现和导出器分离,提供了高效、灵活的分布式追踪和指标采集方案。它的架构保证了可扩展性,允许用户根据需求配置或扩展功能。如果需要更深入的说明,可以指定某个模块或文件!

学习点

OpenTelemetry C++ 项目架构设计中有许多值得学习和借鉴的地方,特别是在构建灵活、高效、可扩展的分布式追踪和指标采集系统方面。以下是一些关键学习点:


1. 模块化设计

  • 特点
    • 项目将功能分解为多个模块,包括 API 层、SDK 层、导出器(Exporter)和工具库(nostd, common)。
    • 每个模块职责单一,易于维护和扩展。
  • 学习点
    • 模块化设计使代码结构清晰,并允许开发者根据需要选择性地引入模块。
    • 提供了灵活性,例如用户可以仅使用 API 层而不依赖具体的 SDK 实现。

2. API 和 SDK 分离

  • 特点
    • API 层提供抽象接口,开发者可以面向接口编程。
    • SDK 层实现了 API 的具体功能,并处理复杂的逻辑,如数据采样、批处理和导出。
  • 学习点
    • 这种分层设计提高了代码的灵活性和可替换性。例如,用户可以用自定义的 SDK 替换默认实现,而无需修改 API 层代码。

3. 可插拔的导出器(Exporter)

  • 特点
    • 项目支持多种导出器(如 Zipkin、Jaeger 和 OTLP),这些导出器实现了相同的接口(SpanExporter)。
    • 用户可以根据需求选择或开发自己的导出器。
  • 学习点
    • 使用接口和工厂模式设计导出器,使系统可以轻松扩展支持新的后端。
    • 通过抽象与实现分离,增强了系统的适配能力。

4. 轻量级工具库(nostd)

  • 特点
    • 提供了一些轻量级的工具类(如 nostd::spannostd::string_view),以减少对外部库的依赖。
    • 设计上兼容 C++11 和 C++14,适应了不同的编译环境。
  • 学习点
    • 在需要兼容老旧环境时,可以通过实现简化版的标准库功能来支持更多用户场景。

5. 高效的并发支持

  • 特点
    • 使用线程安全的数据结构和锁机制(如 std::mutex)来保证多线程环境下的安全性。
    • 例如,BatchSpanProcessor 使用缓冲区和后台线程来批量导出数据,减少了对性能的影响。
  • 学习点
    • 在高性能应用中,通过批处理和异步操作可以有效减少运行时开销。
    • 对并发访问进行严格的控制,避免数据竞争问题。

6. 灵活的配置和扩展机制

  • 特点
    • 支持用户自定义配置(如采样器、处理器和导出器)。
    • 提供了多种默认实现(如简单采样器和批处理器),同时允许用户扩展自己的实现。
  • 学习点
    • 提供默认配置降低了用户的学习成本,而开放扩展接口又满足了高级用户的需求。
    • 使用工厂模式生成对象(如 SpanProcessor::MakeRecordable),进一步增强了灵活性。

7. 跨语言的设计一致性

  • 特点
    • OpenTelemetry 是一个跨语言的项目,C++ 实现遵循了 OpenTelemetry 的整体设计规范,与其他语言(如 Go、Python)的实现保持一致。
  • 学习点
    • 在多语言项目中,保持设计一致性有助于跨团队协作,并降低用户在不同语言中的学习成本。

8. 清晰的目录结构和文档

  • 特点
    • 项目目录按照功能划分,如 apisdkexportersexamplestests
    • 提供了详细的文档和代码示例,帮助用户快速上手。
  • 学习点
    • 清晰的目录结构和完善的文档提升了项目的可维护性和可用性。

9. 测试覆盖与质量保证

  • 特点
    • 提供了丰富的单元测试和集成测试,保证了模块的正确性。
    • 测试覆盖了常见场景和边界情况。
  • 学习点
    • 测试是高质量代码的保障,特别是在分布式系统中,测试可以帮助捕获潜在的复杂问题。

10. 遵循最佳实践和开源规范

  • 特点
    • 遵循 Apache 2.0 开源协议,代码风格整洁,注释清晰。
    • 使用 CMake 管理构建,支持多平台编译。
  • 学习点
    • 遵循开源规范和最佳实践可以提升项目的可读性和社区参与度。

总结

OpenTelemetry C++ 项目的架构体现了模块化、灵活性和高性能的特点。这种设计值得学习,特别是在开发分布式系统、异步处理和可扩展性框架时,可以借鉴其 API/SDK 分离、可插拔设计和高效的并发处理等技术思想。

工厂模式

在 OpenTelemetry C++ 项目中,导出器(Exporter)的设计使用了接口和工厂模式,这种设计模式有助于实现模块化、灵活性和可扩展性。以下是具体解释:


1. 接口的使用

  • 定义:接口是一种抽象类,提供了一组方法的定义,而不包含具体实现。
  • 在导出器中的作用
    • 导出器实现了 SpanExporter 接口,该接口定义了导出追踪数据的标准方法。
    • 用户可以通过使用接口操作导出器,而无需关心其具体实现。
  • 关键接口
    • SpanExporter(位于 sdk/include/opentelemetry/sdk/trace/exporter.h):

      cpp 复制代码
      class SpanExporter
      {
      public:
        virtual ~SpanExporter() = default;
      
        virtual std::unique_ptr<Recordable> MakeRecordable() noexcept = 0;
      
        virtual ExportResult Export(
            const nostd::span<std::unique_ptr<Recordable>> &spans) noexcept = 0;
      
        virtual bool Shutdown(std::chrono::microseconds timeout = (std::chrono::microseconds::max)()) noexcept = 0;
      };
      • MakeRecordable:创建一个 Recordable(可记录的数据结构)。
      • Export:将一批追踪数据导出到后端。
      • Shutdown:关闭导出器并完成清理工作。

2. 工厂模式的使用

  • 定义 :工厂模式提供了一种创建对象的方式,用户只需要调用工厂方法,而不需要直接使用 new 操作符。
  • 在导出器中的作用
    • 工厂模式用于创建导出器实例,如 Zipkin、OTLP 等。
    • 用户可以通过工厂方法动态选择导出器的类型,而无需修改代码。
  • 示例
    • 在某些导出器实现中,MakeRecordable 方法就是一种工厂方法,用于生成 Recordable 对象。例如:

      cpp 复制代码
      std::unique_ptr<sdk::trace::Recordable> ZipkinExporter::MakeRecordable() noexcept
      {
          return std::unique_ptr<sdk::trace::Recordable>(new ZipkinRecordable());
      }
      • 这里 ZipkinExporter 使用工厂方法创建一个 ZipkinRecordable 对象,而用户无需直接调用 new

3. 设计的优势

  • 模块化
    • 不同的导出器(如 OTLP、Zipkin)实现了相同的接口,因此可以很容易替换或扩展新的导出器。
  • 灵活性
    • 工厂模式允许动态创建对象,用户可以根据配置选择导出器类型,而无需修改代码。
  • 可维护性
    • 通过接口分离定义和实现,降低了模块之间的耦合性,便于维护和测试。
  • 可扩展性
    • 新的导出器只需实现接口,并注册到工厂中即可,无需修改现有代码。

4. 实际使用案例

假设用户想使用 Zipkin 导出器:

  1. 接口操作
    用户调用 SpanExporter 定义的方法,例如 Export
  2. 工厂创建
    Zipkin 导出器通过工厂方法 MakeRecordable 创建 Recordable 对象,并将其导出为后端可识别的数据格式。

总结

通过使用接口和工厂模式,OpenTelemetry C++ 项目实现了导出器的模块化和灵活性。接口定义了功能规范,工厂模式提供了动态创建对象的能力,使得用户可以轻松扩展和维护系统,同时支持多种后端。

高并发

OpenTelemetry C++ 项目在高并发环境下的设计,重点在于高效的数据收集、线程安全性以及批量处理机制。以下是对其高并发设计的详细分析:


1. 高效的并发数据收集机制

  • Span 的线程安全设计

    • 在多线程环境下,多个 Span 可能同时被创建和记录数据。
    • Span 的实现中使用了线程安全的机制(如 std::mutex),以确保在高并发环境下数据一致性。
    • 例如在 sdk::trace::Span 的实现中,使用了 mutable std::mutex 锁来保护共享资源。
  • Recordable 的抽象

    • Recordable 是一个线程兼容的类,用于存储追踪数据。
    • 通过接口隔离数据操作逻辑,避免了直接操作共享资源所引发的竞争。

2. 批处理与异步处理

高并发系统中,频繁导出数据会导致性能下降,因此 OpenTelemetry C++ 使用批处理和异步处理来优化性能。

  • 批处理(Batch Processing)

    • OpenTelemetry 提供了 BatchSpanProcessor,用于将多个 Span 数据合并到批次中,再统一导出。
    • 关键点
      • 使用了内部缓冲区(如循环队列)存储待导出的 Span
      • 当缓冲区达到一定大小或超时时,会触发批量导出。
    • 优点
      • 减少了对导出器的调用频率,降低了后端系统的压力。
      • 提高了网络和处理的吞吐量。
  • 异步处理

    • BatchSpanProcessor 使用后台线程异步处理数据:
      • 一个独立的线程定期检查缓冲区,批量发送数据到导出器。
      • 使用条件变量(std::condition_variable)来唤醒工作线程,避免忙等(busy-waiting)。
    • 优点
      • 避免了主线程阻塞,提高了整体系统的响应能力。
      • 允许高并发的 Span 记录,不会受到导出操作的影响。

3. 线程安全的共享资源管理

  • 锁的使用

    • 在需要修改共享资源时(例如缓冲区或 Span 的属性),使用了 std::mutex 或其他同步原语来保护资源。
    • 例如,BatchSpanProcessor 的缓冲区操作实现是线程安全的。
  • 无锁设计

    • 某些场景中,使用了无锁设计来减少锁竞争:
      • 例如,在 Recordable 的创建和记录中,尽量减少对全局状态的依赖。
      • 避免了频繁的上下文切换和锁等待,提高了并发性能。

4. 线程池和资源分配

  • 专用线程
    • BatchSpanProcessor 以及其他批处理器会启动专用线程处理异步任务,而不会影响主线程的性能。
  • 线程池扩展
    • 项目为高负载环境提供了灵活的扩展能力,用户可以通过配置调整线程池的大小或后台线程的行为。

5. 内存优化与延迟控制

高并发场景下,内存使用和延迟控制至关重要。

  • 缓冲区限制

    • 每个 SpanProcessor 都有最大队列大小和最大批量大小的限制,防止内存溢出。
    • 当缓冲区满时,旧数据可能会被丢弃,从而保证系统的稳定性。
  • 超时控制

    • BatchSpanProcessor 支持自定义超时时间,当导出操作超过指定时间时会被强制终止。
    • 这避免了后台线程因单次导出失败而阻塞。

6. 高并发设计的优势总结

  1. 高效的数据收集和处理

    • 多线程环境下,SpanRecordable 的线程安全设计确保了数据一致性。
    • 批处理和异步导出减少了导出器的调用频率,提高了整体吞吐量。
  2. 资源分配合理

    • 使用独立的后台线程,隔离了数据收集和导出的任务,避免相互影响。
  3. 灵活的配置和扩展

    • 用户可以通过调整批处理大小、队列限制和线程数等参数,适应不同的高并发场景。
  4. 可维护性和可扩展性

    • 通过接口和抽象层分离实现,允许用户替换或扩展不同的处理器和导出器。

学习启发

OpenTelemetry C++ 项目在高并发设计中的关键思想包括:

  • 线程安全:在共享资源上使用合理的同步机制。
  • 异步与批处理:减少阻塞操作,提高系统性能。
  • 模块化设计:通过接口和抽象分离逻辑,增强扩展性。
  • 灵活配置:允许用户根据实际需求调整系统行为。

这些设计思路可以广泛应用于高性能和高并发的系统开发中。

跨语言的设计一致性

https://opentelemetry.io/docs/concepts/signals/traces/#spans

QueryPerformanceFrequency

CSCI 4717 -- High Performance Counter

High Resolution Counter

Let's play around with the high resolution counter again. Remember that this counter counts the number of clock cycles of the system clock since the system was last powered up. The function we use to read this timer is QueryPerformanceCounter(). Since function reads the processor's high resolution timer which is incremented once with every clock pulse of the processor's clock, then for a 3.59 GHz machine, this timer is incremented approximately 3,590,000,000 times in a second. QueryPerformanceCounter() takes as its single parameter a pointer to a LARGE_INTEGER which you define in your code. This gives us a way to read how many clock cycles have occurred since the machine was turned on.

cpp 复制代码
LARGE_INTEGER current_time;
QueryPerformanceCounter(&current_time);

The number returned by QueryPerformanceCounter() is huge and not very useful for timing at the "seconds" level. As I mentioned earlier, it is incremented once with every pulse of the system clock. If we knew the system clock frequency, we could figure out how many seconds had passed since turning on the machine. This can be done with QueryPerformanceFrequency(). This function returns the frequency of the processor once again using a LARGE_INTEGER passed as a pointer for the function's only parameter.

cpp 复制代码
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);

Dividing the counter value by the frequency will convert the time to seconds. Dividing the counter value by the frequency divided by 1,000 will convert the time to milliseconds. Dividing the counter value by the frequency divided by 1,000,000 will convert the time to microseconds and so on.

Laboratory Exercise

In today's exercise, we will be using the counter to measure time. For the most part, we will be measuring time in microseconds. Begin by creating a console application in Visual Studio 2005. Below is some sample code that should work well for a template as we go through some of these examples. Remember that LARGE_INTEGER is a UNION, so to access the value stored in it, you must define the way you want the data returned. To do this, use the QuadPart component.

cpp 复制代码
#include "stdafx.h"
#include <iostream>
#include <fstream>
#include "Windows.h"

using namespace std;

int main( )
{
    LARGE_INTEGER current, frequency;
    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&current);
    cout << current.QuadPart/(frequency.QuadPart/1000000) << " uS have passed since resetting the high performance counter.\n";
    return 0;
}

Today, we will see if we can get some semi-accurate measurements of the following programming functions.

Time to read the high performance counter

Time to perform a function call

Time to return from a function call

Time between interruptions from the O/S

Comparison between times to call cout and printf

Comparison between times to perform recursive and non-recursive functions

C++ Windows时间函数 QueryPerformanceCounter()与QueryPerformanceFrequency()

https://zhuanlan.zhihu.com/p/532434005

相关推荐
RLG_星辰3 小时前
第六章-哥斯拉4.0流量分析与CVE-2017-12615的复现
笔记·安全·网络安全·tomcat·应急响应·玄机
敦普水性工业漆8 小时前
汽车紧固件防腐3.0时代:敦普水性漆用无铬锌铝涂层定义「零氢脆」标准
笔记·汽车
TUTO_TUTO9 小时前
【AWS+Wordpress】将本地 WordPress 网站部署到AWS
笔记·学习·云计算·aws
大溪地C10 小时前
CSS详细学习笔记
css·笔记·学习
chennalC#c.h.JA Ptho11 小时前
Centos系统详解架构详解
linux·经验分享·笔记·系统架构·系统安全
饕餮争锋11 小时前
Spring普通配置类 vs 自动配置类-笔记
java·笔记·spring
Aimyon_3612 小时前
Java复习笔记-基础
java·开发语言·笔记
吃货界的硬件攻城狮12 小时前
【STM32 学习笔记】ADC数模转换器
笔记·stm32·单片机·学习
~菜鸟笔记~13 小时前
Git笔记
笔记·git
烟水寻常14 小时前
UE5 诺伊腾动捕使用笔记
笔记·ue5