我怎样才能有效地计时一个只有几个周期长的函数的执行时间?

Posted

技术标签:

【中文标题】我怎样才能有效地计时一个只有几个周期长的函数的执行时间?【英文标题】:How can I effectively time the execution of a function that's only a few cycles long? 【发布时间】:2020-05-27 04:44:06 【问题描述】:

我正在尝试对使用 SSE Intrinsics 计算点积的不同方法进行一些比较,但由于这些方法只有几个周期长,因此我必须运行数万亿次指令才能花费更多时间几分之一秒。唯一的问题是带有-O3 标志的gcc 将我的main 方法“优化”为无限循环。

我的代码是

#include <immintrin.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <inttypes.h>
#define NORMAL 0

struct _Vec3 
    float x;
    float y;
    float z;
    float w;
;

typedef struct _Vec3 Vec3;

__m128 singleDot(__m128 a, __m128 b) 
    return _mm_dp_ps(a, b, 0b00001111);


int main(int argc, char** argv) 
    for (uint16_t j = 0; j < (1L << 16); j++) 
        for (uint64_t i = 0; i < (1L << 62); i++) 
            Vec3 a = i, i + 0.5, i + 1, 0.0;
            Vec3 b = i, i - 0.5, i - 1, 0.0;
            #if NORMAL
            float ans = normalDot(a, b); // naive implementation
            #else
            // float _c[4] = a.x, a.y, a.z, 0.0;
            // float _d[4] = b.x, b.y, b.z, 0.0;
            __m128 c = _mm_load_ps((float*)&a);
            __m128 d = _mm_load_ps((float*)&b);
            __m128 ans = singleDot(c, d);
            #endif
        
    

但是当我使用gcc -std=c11 -march=native -O3 main.c 编译并运行objdump -d 时,它变成了 main

0000000000400400 <main>:
  400400:   eb fe                   jmp    400400 <main>

是否有替代方法来计时不同的方法?

【问题讨论】:

对于小代码 sn-ps,可以使用静态代码分析器,例如 Intel Architecture Code Analyzer 或 LLVM Machine Code Analyzer。 【参考方案1】:

那是因为:

for (uint16_t j = 0; j < (1L << 16); j++) 

是一个无限循环 -- uint16_t 的最大值是 65535 (216-1),之后它将返回 0。因此测试将始终为真。

【讨论】:

哦,这很尴尬。意思是 uint64_t @CalvinGodfrey:修复仍然会优化计算未使用结果的工作。但是,即使使用 Google Benchmark 的 DoNotOptimize 空内联汇编或其他东西来解决这个问题,也会留下更基本的问题,即您是在计时吞吐量还是延迟,这取决于在实际用例中哪个更相关。 编译器应该对此发出警告,启用更多编译器警告。【参考方案2】:

即使在修正了uint16_t 而不是uint64_t 的错字,这会使你的循环无限,实际的工作仍然会被优化掉,因为没有使用结果。

您可以使用 Google Benchmark 的 DoNotOptimize 来阻止您未使用的 ans 结果被优化掉。例如this Q&A is asking about 的“Escape”和“Clobber”等功能。这在 GCC 中有效,并且该问题链接到来自 clang 开发人员的 CppCon 演讲的相关 youtube 视频。

另一种更糟糕的方法是将结果分配给volatile 变量。但请记住,公共子表达式消除仍然可以优化计算的早期部分,无论您使用volatile 还是内联asm 宏来确保编译器在某处实现实际的最终结果。 微基准测试很难。您需要编译器准确地完成在实际用例中会发生的工作量,但不会更多。

请参阅 Idiomatic way of performance evaluation? 了解更多信息。


牢记您在此处测量的确切内容。

根据编译器是否向量化这些初始化器,可能会产生大量循环开销和存储转发停止,但即使这样做;整数到 FP 的转换和 2x SIMD FP 加法在吞吐量成本方面的成本相当 dpps。 (这是您测量的,而不是延迟;根据您的实际用例的上下文,在乱序执行的 CPU 上,差异很重要)。

性能不是一维的,在几个指令的范围内。围绕某些工作进行重复循环可以测量吞吐量延迟,具体取决于您是否使输入依赖于先前的输出(循环携带的依赖链)。但是,如果您的工作最终受限于前端吞吐量,那么循环开销是一个重要部分。另外,由于循环的机器代码如何与 uop 缓存的 32 字节边界对齐,您最终可能会受到影响。

对于这种简短而简单的事情,静态分析通常很好。计算前端的微指令和后端的端口,并分析延迟。 What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?。 LLVM-MCA 可以为您做到这一点,IACA 也可以。您还可以将测量作为使用点积的真实循环的一部分。

另请参阅RDTSCP in NASM always returns the same value,了解有关您可以衡量一条指令的内容的一些讨论。


我必须运行指令数万亿次才能花费不到一秒的时间

当前的 x86 CPU 最多可以在每个时钟周期循环一次迭代,以获得微小的循环。编写一个运行得比这更快的循环是不可能的。在 4GHz CPU 上,40 亿次迭代(以 asm 表示)至少需要一秒钟。

当然,优化的 C 编译器可以展开您的循环,并在每次 asm 跳转时进行尽可能多的源代码迭代。

【讨论】:

以上是关于我怎样才能有效地计时一个只有几个周期长的函数的执行时间?的主要内容,如果未能解决你的问题,请参考以下文章

我怎样才能顺序淡入几个div?

我怎样才能有效地洗牌?

Javascript、内部类以及如何有效地访问父作用域

react——生命周期

我怎样才能有效地做到这一点? [关闭]

我怎样才能有效地找到在预算范围内并最大化效用的活动子集?