代码之家  ›  专栏  ›  技术社区  ›  Partial

在C/C++程序中用汇编程序优化某些函数真的值得吗?

  •  12
  • Partial  · 技术社区  · 15 年前

    在某些开发领域,如游戏开发、实时系统等,有一个快速和优化的程序是很重要的。另一方面,现代编译器已经做了很多优化工作,而在一个截止日期是需要考虑的因素的世界中,在汇编中进行优化是非常耗时的。

    问题:

    1. 正在使用中的程序集优化某些函数 C/C++程序真的值得吗?

    2. 真的有足够的收益吗 优化C/C++的性能 用今天的程序集编程 现代编译器?


    我对所贴答案的理解是,任何可以获得的收益在某些领域都很重要,如嵌入式系统、多媒体编程(图形、声音等)。此外,人们需要有能力(或有人有能力)做一个比现代编译器更好的汇编工作。做一些真正优化的C/C++可以花费更少的时间,并且可以做足够好的工作。最后一点,学习汇编可以帮助理解程序的内部机制,并最终使某人成为更好的程序员。

    11 回复  |  直到 14 年前
        1
  •  27
  •   Boojum    15 年前

    我想说这不值得。我使用的软件可以进行实时3D渲染(即,在没有GPU帮助的情况下进行渲染)。我确实广泛地使用了SSE编译器的内部原理——许多充满了 __mm_add_ps() 还有朋友们——但我很长一段时间内都不需要重新编写汇编中的函数了。

    我的经验是,优秀的现代优化编译器在复杂的微级优化方面非常有效。他们将进行复杂的循环转换,如重新排序、展开、管道化、阻塞、平铺、阻塞、裂变等。他们将安排指令来保持管道的填充,对简单的循环进行矢量化,并部署一些有趣的位旋转黑客。现代编纂者是令人难以置信的迷人的野兽。

    你能打败他们吗?当然,考虑到他们选择启发式使用的优化,他们有时肯定会出错。但我发现,从更大的角度来优化代码本身要好得多。我是否以最适合缓存的方式布局数据结构?我是否在做一些不正统的事情来误导编译器?我可以重写一些东西给编译器更好的提示吗?我最好重新计算一些东西而不是存储它吗?插入预取是否有帮助?我在某个地方有错误的缓存共享吗?编译器是否认为小代码优化不安全,但在这里还可以(例如,将除法转换为乘法的倒数)?

    我喜欢和编译器一起工作,而不是对抗它。让它负责微级别的优化,这样您就可以专注于Mezzo级别的优化。重要的是要了解编译器的工作原理,以便知道两个级别之间的界限在哪里。

        2
  •  10
  •   cletus    15 年前

    唯一可能的答案是:是的,如果有相关和有用的性能增益。

    我应该猜的问题是:在C/C++程序中使用汇编语言能获得有意义的性能增益吗?

    答案是肯定的。

    你得到一个 有意义 在过去的10-20年间,随着库和编译器的改进,性能的提高可能会减少,但对于像x86这样的体系结构,尤其是某些应用程序(特别是与图形相关的应用程序)中的手动优化,可以做到这一点。

    但就像任何事情一样,除非你需要,否则不要优化。

    我认为,在大多数情况下,算法优化和编写高效的C(尤其是)将以比用汇编语言重写花费更少的时间创造更多的性能增益。

        3
  •  6
  •   James Black    15 年前

    困难在于,考虑到现代CPU的体系结构,您能否比编译器做得更好地优化呢?如果您设计的是一个简单的CPU(例如用于嵌入式系统),那么您可以进行合理的优化,但是对于流水线结构,优化要困难得多,因为您需要了解流水线是如何工作的。

    因此,考虑到这一点,如果您可以进行这种优化,并且您正在处理一些探查器告诉您速度太慢的事情,并且这是一个应该尽可能快的部分,那么是的优化是有意义的。

        4
  •  5
  •   DigitalRoss    15 年前

    也许吧

    这完全取决于个人计划

    在知道之前,您需要一个配置文件,它是通过配置文件工具获得的。有些程序把所有的时间都花在等待数据库上,或者只是在一个很小的区域内没有集中的运行时。如果没有这个,组装就没有多大帮助。

    根据经验法则,90%的运行时发生在10%的代码中。你真的想要一个非常强烈的瓶颈,并不是每个程序都有这个瓶颈。

    而且,现在机器速度如此之快,以至于一些低垂的水果被编译器和CPU内核吃掉了,可以说。例如,假设您编写的代码比编译器好,并将指令计数减少一半。即使这样,如果您最终做了相同数量的内存引用,并且它们是瓶颈,那么您可能无法获胜。

    当然,可以在以前的循环迭代中开始预加载寄存器,但编译器可能已经在尝试这样做了。

    学习汇编实际上更重要,因为它是一种理解机器真正是什么的方法,而不是一种击败编译器的方法。但是试试看!

        5
  •  4
  •   sybreon    15 年前

    有一个领域仍然定期进行装配优化- 嵌入式软件 . 这些处理器通常不是很强大,并且有许多架构上的特性,编译器可能无法利用这些特性进行优化。这就是说,它仍然应该只在代码的特别紧凑的区域内完成。 它必须有很好的记录。

        6
  •  4
  •   brianegge    15 年前

    我假设您已经对代码进行了分析,并且找到了一个占用大部分时间的小循环。

    首先,尝试使用更具攻击性的编译器优化重新编译,然后重新分析。如果您已经任意运行了所有编译器优化,并且仍然需要更多的性能,那么我建议您查看生成的程序集。

    在查看了函数的汇编代码之后,我通常会做的是查看如何更改C代码以使编译器能够更好地编写汇编。这样做的好处是,我最终得到的代码可以在我的处理器上与编译器一起运行,但是可以移植到其他环境中。

        7
  •  4
  •   Stephen Canon    15 年前

    对于编写应用程序的典型小型商店开发人员来说,性能增益/工作量权衡几乎从不证明编写程序集是正确的。即使在装配速度可以使某些瓶颈速度翻倍的情况下,这种努力也常常是不合理的。在一家大公司里,如果你是“表现型员工”,这可能是合理的。

    然而,对于一个库编写者来说,即使是小的改进也常常是合理的,因为它为最终使用库的数千开发人员和用户节省了时间。对编译器编写者来说更是如此。如果您能在核心系统库功能中获得10%的效率胜利,那么就可以真正节省分布在您的用户群上的数千年(或更长)电池寿命。

        8
  •  2
  •   Lior Kogan    15 年前

    当然可以!

    这里是一个CRC-32计算的演示,我用C++编写,然后用VisualStudio在X86汇编程序中进行优化。

    应在程序启动时调用initcr32table()。 calcCrc32()将计算给定内存块的CRC。 这两种功能都是在汇编和C++中实现的。

    在一个典型的奔腾机器上,你会注意到汇编程序CalcCRC32()函数比C++代码快50%。

    汇编程序实现不是MMX或SSE,而是简单的x86代码。 编译器将永远不会生成像手工编制的汇编程序代码那样高效的代码。

        DWORD* panCRC32Table = NULL; // CRC-32 CCITT 0x04C11DB7
    
        void DoneCRCTables()
        {
            if (panCRC32Table )
            {
                delete[] panCRC32Table;
                panCRC32Table= NULL;
            }
        }
    
        void InitCRC32Table()
        {
            if (panCRC32Table) return;
            panCRC32Table= new DWORD[256];
    
            atexit(DoneCRCTables);
    
        /*
            for (int bx=0; bx<256; bx++)
            {
                DWORD eax= bx;
                for (int cx=8; cx>0; cx--)
                    if (eax & 1)
                        eax= (eax>>1) ^ 0xEDB88320;
                    else
                        eax= (eax>>1)             ;
                panCRC32Table[bx]= eax;
            }
        */
                _asm cld
                _asm mov    edi, panCRC32Table
                _asm xor    ebx, ebx
            p0: _asm mov    eax, ebx
                _asm mov    ecx, 8
            p1: _asm shr    eax, 1
                _asm jnc    p2
                _asm xor    eax, 0xEDB88320           // bit-swapped 0x04C11DB7
            p2: _asm loop   p1
                _asm stosd
                _asm inc    bl
                _asm jnz    p0
        }
    
    
    /*
    DWORD inline CalcCRC32(UINT nLen, const BYTE* cBuf, DWORD nInitVal= 0)
    {
        DWORD crc= ~nInitVal;
        for (DWORD n=0; n<nLen; n++)
            crc= (crc>>8) ^ panCRC32Table[(crc & 0xFF) ^ cBuf[n]];
        return ~crc;
    }
    */
    DWORD inline __declspec (naked) __fastcall CalcCRC32(UINT        nLen       ,
                                                         const BYTE* cBuf       ,
                                                         DWORD       nInitVal= 0 ) // used to calc CRC of chained bufs
    {
            _asm mov    eax, [esp+4]         // param3: nInitVal
            _asm jecxz  p2                   // __fastcall param1 ecx: nLen
            _asm not    eax
            _asm push   esi
            _asm push   ebp
            _asm mov    esi, edx             // __fastcall param2 edx: cBuf
            _asm xor    edx, edx
            _asm mov    ebp, panCRC32Table
            _asm cld
    
        p1: _asm mov    dl , al
            _asm shr    eax, 8
            _asm xor    dl , [esi]
            _asm xor    eax, [ebp+edx*4]
            _asm inc    esi
            _asm loop   p1
    
            _asm pop    ebp
            _asm pop    esi
            _asm not    eax
        p2: _asm ret    4                    // eax- returned value. 4 because there is 1 param in stack
    }
    
    // test code:
    
    #include "mmSystem.h"                      // timeGetTime
    #pragma comment(lib, "Winmm.lib" )
    
    InitCRC32Table();
    
    BYTE* x= new BYTE[1000000];
    for (int i= 0; i<1000000; i++) x[i]= 0;
    
    DWORD d1= ::timeGetTime();
    
    for (i= 0; i<1000; i++)
        CalcCRC32(1000000, x, 0);
    
    DWORD d2= ::timeGetTime();
    
    TRACE("%d\n", d2-d1);
    
        9
  •  1
  •   Nick Bedford    15 年前

    我想说,对于大多数人和大多数应用程序来说,这是不值得的。编译器非常擅长精确地优化为其编译的体系结构。

    这并不是说优化装配并不是没有必要的。许多数学和低级密集型代码通常通过使用特定的CPU指令(如SSE*等)来优化,以克服编译器生成的指令/寄存器的使用。最后,人类精确地知道程序的要点。编译器只能假设这么多。

    我会说,如果您不在您知道自己的程序集将更快的级别上,那么我会让编译器做这项艰苦的工作。

        10
  •  1
  •   sharptooth    15 年前

    不要忘记,在程序集中重写会丢失可移植性。今天你不在乎,但明天你的客户可能会想要你的软件在另一个平台上,而那些组装片段会真的伤害他们。

        11
  •  1
  •   Community clintgh    7 年前

    好答案。如果你已经做了,我会说“是的” performance tuning like this 你现在处于

    1. 知道(不是猜测)某个特定的热点占用了你30%以上的时间,

    2. 看到编译器为它生成了什么汇编语言,在尝试使它生成最佳代码之后,

    3. 知道如何改进汇编程序代码。

    4. 愿意放弃一些便携性。

    编译器不知道你所知道的一切,所以它们是防御性的,不能利用你所知道的。

    作为一个例子,它们以一种通用的方式编写子例程入口和出口代码,不管子例程包含什么内容,都能正常工作。另一方面,您可以手工编写一些小的例程,这些例程可以省去帧指针、保存寄存器和类似的东西。你冒着bug的风险,但有可能击败编译器。