繁体   English   中英

确定性的执行时间度量

[英]A deterministic execution time measure

一些算法依赖于时间度量。 例如,10% 的时间,遵循方法 A。如果这不起作用,则在 20% 的时间遵循 B。 如果这不起作用,请执行 C。

以秒为单位测量执行时间是不确定的。 缓存状态、在内核上交错非用户任务,甚至只是现代处理器时钟速度的动态提升都是改变其他确定性代码执行时间的外部影响。 因此,如果使用经典的执行时间度量,该算法可能会表现出非确定性。

为了保持算法的行为确定性,我正在寻找一种确定性的方法来测量执行时间。 这是可能的,例如,CPLEX 求解器具有称为ticks的确定性时间度量。

我知道这个简单的问题没有简单的答案。 所以让我把它缩小一点:

  • 确定性属性是一个硬约束。 我宁愿有一个与测量的执行时间只有非常微弱相关的度量,只要它是确定性的。
  • 理想情况下,确定性时间度量测量整个程序执行,包括静态编译的库。 但如果这是不可能的,那么测量我可以修改的源代码的执行时间就可以了。
  • 我愿意承受 100% 的性能损失,但不会更多。 不过,性能影响越小越好:)
  • 如果编译后的二进制文件在不同 CPU 型号之间不再可移植,那也没关系。

我考虑过一些方法,但不知道它们实施起来有多难或它们的效果如何:

  • 修改编译器以在编译代码中的每个其他命令之间添加一个增加全局计数器的命令。 这似乎是最有原则的方法,理论上甚至可能适用于静态编译的库。
  • 计算内存访问次数。 不知道如何在实践中做到这一点。 可能也通过修改编译器?
  • 使用源代码中的全局计数器计算 if 语句和循环条件检查的数量。 这可以通过例如宏轻松完成,但它会忽略许多库调用(例如,对向量进行排序的简单调用不会增加计数器),因此可能与实际执行时间没有太大关联。
  • 访问硬件性能计数器,例如,计算进程的指令数,可能是通过诸如PAPI 之类的库。 这里的问题是我认为这些计数器也是不确定的?

那么,如何确定性地衡量程序的执行时间呢?

编辑:测量 CPU 时间(例如通过clock()函数)绝对比我天真的挂钟时间示例更好。 但是,测量 CPU 时间绝不是确定性的:运行相同的确定性程序将产生不同的 CPU 时间。 我真的在寻找一个确定性的度量(或@mevets 所说的“完成的工作”的度量)。

您可以通过调用 C 标准库函数clock()访问进程时间(进程使用的时钟周期数)而不是挂钟时间(经过的时间,包括在其间进行上下文切换的任何其他进程clock() 一秒钟内有CLOCKS_PER_SEC时钟滴答。 请注意,如果您的程序是多线程的,这可能比挂钟时间运行得更快——即,它测量程序在所有处理器内核上消耗的时钟周期。 因此, CLOCKS_PER_SEC时钟滴答是指一个处理器内核上的一秒计算时间。 要实现方法之间的切换,您可以使用异步 I/O(例如使用新奇的 C++20 协程或 Boost 协程),偶尔检查进程时间,或者您可以执行定时软件中断,设置一个标志由主执行线程执行,然后切换到新方法。

您可能不想在每条指令后增加计数器。 这会产生巨大的计算开销并占用您的处理器管道,因为所有其他指令都依赖于它之前的指令 2,以及您的指令缓存。

代码示例(POSIX):

static /* possibly thread_local */ std::atomic<int> method;
void interrupt_handler(int signal_code) {
    method.fetch_add(1);
}

void calculation(/* input */) {
    auto prev_signal_handler = signal(SIGINT, &interrupt_handler);
    
    try {
        method.store(0);
        int prev_method = 0;

        // schedule timer interrupts
        for (size_t num_ns : /* list of times, in ns */) {
            timer_t t_id;
            sigevent ev;
            ev.sigev_notify = SIGNAL;
            ev.sigev_signo = SIGINT;
            ev.sigev_value.sival_ptr = &t_id;
            timer_create(CLOCK_THREAD_CPUTIME_ID, &ev, &t_id);
            itimerspec t_spec;
            t_spec.it_interval.tv_sec = t_spec.it_value.tv_sec = num_ns / 1000000000;
            t_spec.it_interval.tv_nsec = t_spec.it_value.tv_nsec = num_ns % 1000000000;
            timer_settime(t_id, 0, &t_spec, nullptr);
        }

        bool done = false;
        while (!done) {
            int current_method = method.load();
            if (current_method != prev_method) {
                // switch method
            }
            else {
                // continue using current method
            }
        }
    }
    catch (...) {
        signal(SIGINT, prev_signal_handler);
        throw;
    }
    
    signal(SIGINT, prev_signal_handler);
}

您陷入了一些可能会广泛更改代码的详细解决方案中,可能是因为这些是您熟悉的唯一方法,但恕我直言,这是短视的。 此时您无法确定以这种侵入性方式检测生成的代码是否有价值。 让我们退后一步。

一些算法依赖于时间度量。 例如,10% 的时间,遵循方法 A。如果这不起作用,则在 20% 的时间遵循 B。 如果这不起作用,请执行 C。

我不认为这是真的。 这是一个任意约束,根本不通用。 算法依赖于“努力”,而且通常实时是努力的一个非常糟糕的替代品。 正如您所说的那样,任何类型的“时间”都深陷于架构细节中。

另一个问题是假设算法是变化的单位。 它们通常不是,即您在这里没有您想象的那么多控制权,除非您在汇编中对所有数字部件进行编码,或者彻底审核生成的代码。 由于生成的代码在运行时所做的与体系结构相关的选择,每个算法如果成功,可能会产生略有不同的结果,具体取决于数值错误堆栈。 这是一回事,编译器和/或它们的运行时库做了很多! 因此,只要您的目标是显示它不正确,在各种 PC 上运行相同的编译浮点代码并产生位相同结果的想法是正确的,但实际上它会在稍后的某个时间证明是不正确的太深入它而无法实际实施修复所需的巨大更改。

但是在算法内部,您应该有很多可以增加计数器的任意点 - 不要太频繁,并使用计数器的值作为算法所花费的努力的衡量标准。 对于每种算法,这种度量具有与“实时”不同的比例因子并不重要,因为这里的真正要求不是实时。 您真正想要的是某种确定性的方式来执行切换算法的决定,并且您可以将这些任意切换点粗略地校准为实时一次,并保持这种校准冻结:这并不重要,只要您可以清楚地决定何时切换。

此外,当算法产生非常接近努力阈值的结果(“收敛”)时,需要注意一些问题。 由于架构差异,在固定浮点阈值方面实现“收敛”所需的确切工作可能在 CPU 代之间略有不同。 因此,不是硬截止,您需要某种表达滞后的方式,以便如果收敛发生在努力截止附近,则使用更多替代标准用于阈值或收敛,但您需要做适当的统计建模以表明替代方案足够可靠。

计数器可以处理工作单位,但每个单位的价值(即时间)是否相等? 服务时钟(3) 提供了一个近似的虚拟执行时间——即您的进程实际运行时经过的时间,而不是现实世界(墙)时间。

类似地,timer_create 可以接受类似于 CLOCK_PROCESS_CPUTIME_ID 的时钟 ID,它允许您在某个 CPU 时间过去后发出信号。 如果您的应用程序可以在不进入未定义状态的情况下被任意中断,您可以使用它从方法 1 -> 2 -> 3 切换。

尽管比计算工作块要好,但您需要接受准确时间周围的某些不准确性,以考虑系统开销、缓存争用等。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM