• 一条指令优化引发的血案,性能狂掉50%,clang使用ffastmath选项后变傻了


    https://www.cnblogs.com/bbqzsl/p/15510377.html

    近期在做优化时,对一些函数分别在不同编译平台上进行bench测试。发现了不少问题。

    现在拿其中一个问题来分享。

     1 typedef float MAFloat;
     2 
     3 MAFloat sma(const MAFloat* seq, const int cnt, const int N, const int M)
     4 {
     5     const MAFloat C1 = (MAFloat)M/N;
     6     const MAFloat C2 = (MAFloat)(N-M)/N;
     7     MAFloat result = 0.f;
     8     int total = cnt;
     9 
    10 #pragma nounroll
    11     for (int i = 0; i < total; ++i)
    12     {
    13         result = result * C2 + seq[i] * C1;
    14     }
    15 
    16     return result;
    17 }

    测试代码很简单,只一个循环,循环内只做了算术运算,汇编代码也很容易。

    测试平台包括:

    win10:平台,vc120,gcc10,clang11

    centos8:平台,gcc8,gcc10,clang11

    vc:使用选项 /arch:sse2 /O2,并且win32

    gcc:使用选项 -ffast-math -O2 -m32

    clang:使用选项 -ffast-math -O2 -m32

    数组长度为 28884 = 7221 * 4;

    cpu 是 core i5,3.5Ghz

    测试结果:

    win10:平台,vc120 (0.06x ms),gcc10 (0.06x ms),clang11 (0.09x ms)

    centos8:平台,gcc8 (0.06x ms),gcc10 (0.06x ms),clang11 (0.09x ms)

    不论在win10还是centos8平台上,clang编译的代码的性能居然比vc或gcc编译的代码性能差了50%。

    现在我们来对比gcc10与clang11产出汇编代码

    ## gcc 
    
    .L149:            
        movss    (%edx,%eax,4), %xmm1    # xmm1 = seq[i]
        mulss    %xmm3, %xmm0         # xmm0 = result * C2
        addl    $1, %eax            # 
        mulss    %xmm2, %xmm1         # xmm1 = seq[i] * C1 
        addss    %xmm1, %xmm0         # result = xmm0 + xmm1
        cmpl    %ecx, %eax    
        jl    .L149               # next loop
    
    
    ## clang
    
    LBB7_3:                                 # =>This Inner Loop Header: Depth=1            
        movss    (%eax,%edx,4), %xmm4            # xmm4 = mem[0],zero,zero,zero    
        mulss    %xmm1, %xmm3           # 
        incl    %edx    
        cmpl    %ecx, %edx    
        mulss    %xmm0, %xmm4    
        addss    %xmm4, %xmm3    
        mulss    %xmm2, %xmm3           # xmm2 = 1/N;
        jl    LBB7_3    

    gcc生成的汇编代码一共7条指令,clang生成的汇编代码一共8条指令多出了一条mulss。

    clang不知什么原因自作聪明将

    result * C2 + seq[i] * C1;

    优化成

    (1/N) * (result * (N-M) + seq[i] * M);

    即使多出一条mulss指令,性能也不至于差了50%,就像7条指令与10.5条指令的差距。

    现在来分析

    我的机器使用i5 3.5Ghz, 1ns可以运行3.5指令周期。

    数组长度为28884,即执行循环代码28884次

    运行时间为 28884 * (循环体指令周期)/  3.5

    我现在粗略地将每条指令周期看作是1,gcc生成的代码运行时间粗略地为 28884 * 7 / 3.5 = 57768ns,与测试结果在0.06ms基本相当。用同样的方法估算,clang生成的代码运行时间粗略地为 28884 * 8 / 3.5 = 66020ns。

    但是不同的指令,执行不同数量的微指令(uop),也就是延迟,mulss为4或5,addss为3,上面汇编代码的其它指令各为1。

        mulss    %xmm2, %xmm1         # xmm1 = seq[i] * C1 
        addss    %xmm1, %xmm0         # result = xmm0 + xmm1

    在上面两条指令,addss 依赖 mulss 的结果于 %xmm1,也就是说addss 必须在mulss开始执行后延迟4或5个周期才能执行。由于cpu的乱序机制,这时候延迟的周期数内可以在其他ALU执行其它指令。所以gcc生成的汇编代码的情况可以看作没有指令周期的损失。

    再来看clang生成的汇编代码

        mulss    %xmm0, %xmm4    
        addss    %xmm4, %xmm3    
        mulss    %xmm2, %xmm3           # xmm2 = 1/N;

    addss 依赖 mulss 的结果于 %xmm4,然后mulss 依赖 addss 的结果于 %xmm3,这里我们将第一个依赖等同于gcc汇编中的那个依赖,那么下一个依赖的3个周期就必须等待,一次循环一共才8条指令,两个依赖的延迟合计就8个指令周期,乱序也就没有指令可以执行,所以就硬生生多出3或4个指令周期等待。

    运行时间一下子就变成了 28884 * (8+3) / 3.5 = 90778ns。

    估算的结果与测试的结果基本上吻合。

    有兴趣的朋友可以到godblot上测试汇编,一旦让clang使用-ffast-math选项,编译发生这一出傻事。

    ----------------------------------------------

    另外本人开发了一个支持c++14编写K线技术公式的平台工具 KTL。目前最新版支持Qt5编写窗口工具。

    简介https://www.cnblogs.com/bbqzsl/p/15195253.html

    github仓库

    国内仓库https://gitee.com/bbqz007/KTL 

    逆向深入objc,c++ windows下仿objc动画层UI引擎
  • 相关阅读:
    angular2 UT 导入 jquery问题解决
    css超过指定宽度用...表示
    karma-coverage通过浏览器显示
    angular2复选框及其按钮
    前端分页控制
    input复选框checkbox默认样式纯css修改
    弧形侧边栏
    浅谈软件测试
    随笔1
    java注解小记
  • 原文地址:https://www.cnblogs.com/bbqzsl/p/15510377.html
Copyright © 2020-2023  润新知