间接成本 ~ 浮点乘法的 3 倍,真的吗? (带演示)

Posted

技术标签:

【中文标题】间接成本 ~ 浮点乘法的 3 倍,真的吗? (带演示)【英文标题】:indirection cost ~ 3x of float multiplication, really? (with demo) 【发布时间】:2017-10-25 13:59:10 【问题描述】:

我刚刚发现间接成本大约是浮点乘法的 3 倍! 这是可以预期的吗?我的测试错了吗?

背景

阅读How much does pointer indirection affect efficiency? 后,我对间接成本感到恐慌。

由于现代 CPU 的工作方式,通过指针间接寻址可能会慢得多。

在我过早地优化我的真实代码之前,我想确保它真的像我担心的那样花费很多。

我做了一些技巧来找到粗略的数字(3x),如下所示:-

步骤 1

Test1 : 无间接 -> 计算一些东西 Test2 :间接 -> 计算一些东西(相同)

我发现 Test2Test1 花费更多时间。 这里没有什么奇怪的。

第二步

Test1 : 无间接 -> 计算一些昂贵的东西 Test2:间接 -> 计算一些便宜的东西

我尝试将calculate something expensive 中的代码一点一点地更改为更昂贵,以使两个测试 的成本几乎相同。

结果

最后,我发现使两个测试使用相同时间量(即收支平衡)的可能功能之一是:-

Test1 : 无间接 -> 返回float*float*... 3 次 Test2 : 间接 -> 只需返回 float

这是我的测试用例 (ideone demo) :-

class C
    public: float hello;  
    public: float hello2s[10];  
    public: C()
        hello=((double) rand() / (RAND_MAX))*10;
        for(int n=0;n<10;n++)
            hello2s[n]= ((double) rand() / (RAND_MAX))*10;
        
    
    public: float calculateCheap()
        return hello;
    
    public: float calculateExpensive()
        float result=1;
        result=hello2s[0]*hello2s[1]*hello2s[2]*hello2s[3]*hello2s[4];
        return result;
    
;

这里是主要的:-

int main()
    const int numTest=10000;
    C  d[numTest];
    C* e[numTest];
    for(int n=0;n<numTest;n++)
        d[n]=C();
        e[n]=new C();
    
    float accu=0;
    auto t1= std::chrono::system_clock::now();
    for(int n=0;n<numTest;n++)
        accu+=d[n].calculateExpensive();  //direct call
    
    auto t2= std::chrono::system_clock::now();
    for(int n=0;n<numTest;n++)
        accu+=e[n]->calculateCheap();     //indirect call
    
    auto t3= std::chrono::system_clock::now();
    std::cout<<"direct call time ="<<(t2-t1).count()<<std::endl;
    std::cout<<"indirect call time ="<<(t3-t2).count()<<std::endl;
    std::cout<<"print to disable compiler cheat="<<accu<<std::endl;

直接通话时间间接通话时间调整为与上述相似(通过编辑calculateExpensive)。

结论

间接成本 = 3x 浮点乘法。在我的桌面(带有 -O2 的 Visual Studio 2015)中,它是 7x。

问题

是否可以预期间接成本约为浮点乘法的 3 倍? 如果不是,我的测试怎么错了?

(感谢enhzflep提出改进建议,已编辑。)

【问题讨论】:

一个public: 访问说明符就足够了。您还使用 new 运算符泄漏内存。如果你想使用 C++,我建议你忘记你所知道的关于 Java 的一切。 @Raw N 是的,先生!但我觉得(java)这样回家。 XD @javaLover - 在没有看到编译器生成的代码的情况下,我不太确定您实际上是在将 13 次乘法的成本与间接相乘的成本进行比较。大多数现代编译器都足够聪明,可以避免从您提供的源代码生成执行 13 次乘法运算的代码。更好的测试是将不同的变量相乘,从而强制编译器生成与源代码包含的一样多的乘法。例如:a*b*c*d*e*f*g*h*i*j*k*l*m*n @enhzflep 哦,我的错。我改进后,它变成了3x-6x。 (编辑添加)谢谢。在您看来,现在是一个合理的数字吗? 还应该注意的是,您的成本的很大一部分不会纯粹是由于间接性,而是更有可能部分是由于内存碎片。请注意,您调用了 new C() 100'000 次。这将在你的内存中创建 100'000 个 C scatter 实例。分配为数组 (new C[numTest]) 可能会产生完全不同的结果。次要补充:像这样C d[numTest] = ; 初始化将在每个元素上调用构造函数 【参考方案1】:

间接成本主要由缓存未命中率决定。因为老实说,缓存未命中比您所说的任何其他东西都要昂贵得多,其他一切最终都是舍入错误。

缓存未命中和间接寻址可能比您的测试表明的要昂贵得多。

这主要是因为您只有 100,000 个元素,而 CPU 缓存可以缓存每一个这些浮点数。顺序堆分配将趋于聚集。

你会得到一堆缓存未命中,但不是每个元素都有一个。

你的两个案例都是间接的。 “间接”情况必须遵循 两个 指针,而“直接”情况必须执行一个指针算术实例。 “昂贵”的情况可能适用于某些 SIMD,尤其是在浮点精度较宽松(允许乘法重新排序)的情况下。

正如here 或this image 所见(不是内联,我没有权利),主内存引用的数量将支配上述代码中的几乎所有其他内容。 2 Ghz CPU 的周期时间为 0.5 ns,主内存参考为 100 ns 或 200 个周期的延迟。

同时,如果您可以执行矢量化代码,桌面 CPU 每个周期可以进行 8 次以上的浮点运算。浮点运算的速度可能比单个缓存未命中快 1600 倍。

间接可能会使您无法使用矢量化指令(8 倍减速),并且如果所有内容都在缓存中,则仍然可能比替代方案更频繁地需要 L2 缓存引用(14 倍减速)。但与 200 ns 的主存储器参考延迟相比,这些减速很小。

请注意,并非所有 CPU 都具有相同级别的矢量化,正在努力加快 CPU/主内存延迟,FPU 具有不同的特性,以及无数其他复杂情况。

【讨论】:

谢谢The "indirect" case has to follow two pointers 赐教。嗯,有趣的是,像这样的每一个好的信息表总是缺少算术运算的成本。 @java 1 个周期或更少;通过矢量化,你可以做很多/循环。到底有多少是棘手的。超线程使它变得棘手。导致降频的 TPD 也使其变得棘手。但是“1 个周期”是一个很好的起点。【参考方案2】:

坦率地说,您的测试非常不具代表性,实际上并不能准确衡量您可能认为的结果。

请注意,您拨打了 new C() 100'000 次。这将在你的内存中创建 100'000 个 C 实例,每个实例都非常小。如果您的内存访问是定期的,现代硬件非常擅长预测。由于每次分配,每次调用 new 都是独立发生的,内存地址不会很好地组合在一起,这使得预测变得更加困难。这会导致所谓的缓存未命中。

分配为数组 (new C[numTest]) 可能会产生完全不同的结果,因为在这种情况下地址又是非常可预测的。将您的内存尽可能紧密地组合在一起并以线性、可预测的方式访问它通常会提供更好的性能。这是因为大多数缓存和地址预取器都希望这种模式出现在普通程序中。

小补充:像这样初始化C d[numTest] = ; 将调用每个元素的构造函数

【讨论】:

我只是测试关于内存碎片的假设(避免 new())。结果表明性能损失可能不是由于new() 100000 次或内存碎片。 (但可能来自缓存未命中)ideone.com/p7QpcY (result=3-4x) ..... 是的,使用 new C[numTest] 创建它并按顺序访问它确实使成本 = 1 乘法。 @javaLover 请在另一个评论线程上查看我的回答,这种解释不能证明任何这些陈述。另请注意,new() 的成本可能高度取决于系统。【参考方案3】:

您的问题没有简单的答案。这取决于硬件的功能和特性(CPU、RAM、总线速度等)。

在过去,浮点乘法可能需要数十个甚至数百个周期。内存访问的速度与 CPU 频率相似(想想这里的 MegaHertz),并且浮点乘法比间接访问需要更长的时间。

从那时起,情况发生了很大变化。现代硬件可以在一两个周期内执行浮点乘法,而间接(内存访问)可能只需要几个周期到数百个周期,具体取决于要读取的数据所在的位置。可以有多个级别的缓存。在极端情况下,通过间接访问的内存已交换到磁盘并需要重新读入。这将有数千个周期的延迟。

通常,获取浮点乘法操作数和解码指令的开销可能比实际乘法时间长。

【讨论】:

以上是关于间接成本 ~ 浮点乘法的 3 倍,真的吗? (带演示)的主要内容,如果未能解决你的问题,请参考以下文章

C# 中的浮点数学是不是一致?是真的吗?

向量矩阵乘法、浮点向量、二进制矩阵

是否允许编译器优化浮点常量乘法

矩阵乘法无需相乘,速度提升100倍,MIT开源最新近似算法 | ICML 2021

矩阵乘法无需相乘,速度提升100倍!MIT开源最新近似算法!

矩阵乘法无需相乘,速度提升100倍!MIT开源最新近似算法!