【问题标题】:Faster approach to checking for an all-zero buffer in C?在 C 中检查全零缓冲区的更快方法?
【发布时间】:2010-12-02 09:51:21
【问题描述】:

我正在寻找一种更快的方法来实现这一点:

int is_empty(char * buf, int size) 
{
    int i;
    for(i = 0; i < size; i++) {
        if(buf[i] != 0) return 0;
    }
    return 1;
}

我意识到我正在寻找一种不必要的微优化,除非在极端情况下,但我知道存在一种更快的方法,我很好奇它是什么。

【问题讨论】:

  • 笑话答案:int is_empty(char *buf, int size) { memset(buf, 0, size); return 1; }
  • 附带说明,对于表示数组大小的值,您应该真正使用size_t 而不是int
  • 听起来像是达夫设备的工作!
  • @Rob - 你很可能会被磁盘绑定,不应该专注于此。在现代 CPU 上,10 毫秒的磁盘读取或写入是数百万个时钟周期。优化读取磁盘的方式将获得更好的结果。
  • @Rob: kernelnewbies.org/Linux_2_6_28 看看 1.13 FIEMAP

标签: c performance optimization buffer


【解决方案1】:

在许多架构上,比较 ​​1 个字节与 4 个或 8 个字节所花费的时间相同,有时甚至是 16 个。4 个字节通常很容易(int 或 long),而 8 个字节太容易(long 或 long long)。 16 或更高版本可能需要内联汇编,例如,使用向量单元。

另外,一个分支的错误预测真的很伤人,它可能有助于消除分支。例如,如果缓冲区几乎总是空的,而不是针对 0 测试每个块,而是将它们位或一起测试最终结果。


在可移植 C 中表达这一点很困难:将 char* 转换为 long* 违反了严格的别名。但幸运的是,您可以使用memcpy 可移植地表达一个未对齐的多字节负载,它可以给任何东西起别名。编译器会将其优化为您想要的 asm。

例如,Godbolt 编译器资源管理器上的这个正在进行中的实现 (https://godbolt.org/z/3hXQe7) 表明,通过加载两个连续的 uint_fast32_t 变量(通常为 64- bit) 使用 memcpy 然后检查 tmp1 | tmp2,因为许多 CPU 会根据 OR 结果设置标志,所以这可以让您检查两个单词的价格。

在没有高效未对齐加载的情况下,让它为目标高效编译需要在启动代码中进行一些手动对齐,即使这样,gcc 也可能不会内联 memcpy 以用于无法证明对齐的加载。

【讨论】:

  • +1 表示分支信息,因为发布者正在寻找微优化。
  • 请注意,将 1 个字节的组作为单个整数值进行比较会带来对齐问题,这使得安全操作变得困难。
  • 关于分支预测,您无法真正优化此循环。如果没有预测信息,现代处理器将倾向于向后跳转而不是跌落,之后它将继续正确预测分支,直到循环结束。最终结果是每个缓冲区出现一次错误预测,我无法想象有一种方法可以将其降至 0。
  • 他不是在谈论循环的预测,他是在谈论测试缓冲区内容为零的预测(并通过更少比较来改进它)
  • 对齐不会让事情变得太复杂。一次处理一个字节,直到获得必要的对齐,然后进入使用较大比较的主循环,并在完成后进行一些单字节比较以清理剩余项目。仅当您处理可能具有不同相对对齐方式的多个缓冲区时,对齐才是一个巨大的痛苦。
【解决方案2】:

一种可能的方式,灵感来自Kieveli 被驳回的想法:

int is_empty(char *buf, size_t size)
{
    static const char zero[999] = { 0 };
    return !memcmp(zero, buf, size > 999 ? 999 : size);
}

请注意,您不能使此解决方案适用于任意尺寸。你可以这样做:

int is_empty(char *buf, size_t size)
{
    char *zero = calloc(size);
    int i = memcmp(zero, buf, size);
    free(zero);
    return i;
}

但是任何动态内存分配都会比你所拥有的要慢。第一个解决方案更快的唯一原因是因为它可以使用memcmp(),它将由库编写者用汇编语言手动优化,并且比您用 C 编写的任何代码都要快。

编辑:根据先前关于缓冲区处于状态 X 的“可能性”的观察,没有其他人提到过的优化:如果缓冲区不为空,它是否更可能在开始时不为空或结束?如果最后更有可能出现杂乱无章的情况,您可以在最后开始检查,可能会看到不错的一点性能提升。

编辑 2:感谢 cmets 中的 Accipitridae:

int is_empty(char *buf, size_t size)
{
    return buf[0] == 0 && !memcmp(buf, buf + 1, size - 1);
}

这基本上将缓冲区与自身进行比较,并首先检查第一个元素是否为零。这样,任何非零元素都会导致memcmp() 失败。我不知道这与使用另一个版本相比如何,但我知道如果第一个元素非零,它将很快失败(甚至在我们循环之前)。如果你更可能在最后出现杂物,请将buf[0] 更改为buf[size] 以获得相同的效果。

【讨论】:

  • 另一方面,memcmp 版本会受到内存带宽的限制,如果编译器不太笨,更智能的 C 版本也会如此,但 C 版本会有 一半 要读取的内存。无需尝试,我预测 C 版本至少比 memcmp 快 50%。
  • a) 您比较的是哪些“C”版本? b) 为什么memcmp() 需要更长的时间?
  • 您可以使用以下方法代替分配内存:buf[0]==0 && !memcmp(buf, buf+1, size-1)。
  • @Pascal - 计时,第一个 memcmp() 版本的速度几乎与 OP 的版本相同,始终稍微(例如,0.001 秒)更快。
  • 您可能也想创建 static 数组 const。该版本可以简单地内联,这可能是另一个(微小的)胜利。
【解决方案3】:

上面给出的基准 (https://stackoverflow.com/a/1494499/2154139) 并不准确。它们暗示 func3 比其他选项快得多。

但是,如果您更改测试的顺序,使 func3 位于 func2 之前,您会发现 func2 快得多。

在单次执行中运行组合基准测试时要小心...副作用很大,尤其是在重用相同变量时。最好单独运行测试!

比如改成:

int main(){
  MEASURE( func3 );
  MEASURE( func3 );
  MEASURE( func3 );
  MEASURE( func3 );
  MEASURE( func3 );
}

给我:

func3: zero          14243
func3: zero           1142
func3: zero            885
func3: zero            848
func3: zero            870

这真的让我很烦恼,因为我看不出 func3 怎么能比 func2 执行得这么快。

(为回答道歉,而不是作为评论,没有声誉)

【讨论】:

    【解决方案4】:

    四个函数用于通过简单的基准测试来测试缓冲区的零性:

    #include <stdio.h> 
    #include <string.h> 
    #include <wchar.h> 
    #include <inttypes.h> 
    
    #define SIZE (8*1024) 
    char zero[SIZE] __attribute__(( aligned(8) ));
    
    #define RDTSC(var)  __asm__ __volatile__ ( "rdtsc" : "=A" (var)); 
    
    #define MEASURE( func ) { \ 
      uint64_t start, stop; \ 
      RDTSC( start ); \ 
      int ret = func( zero, SIZE ); \ 
      RDTSC( stop ); \ 
      printf( #func ": %s   %12"PRIu64"\n", ret?"non zero": "zero", stop-start ); \ 
    } 
    
    
    int func1( char *buff, size_t size ){
      while(size--) if(*buff++) return 1;
      return 0;
    }
    
    int func2( char *buff, size_t size ){
      return *buff || memcmp(buff, buff+1, size-1);
    }
    
    int func3( char *buff, size_t size ){
      return *(uint64_t*)buff || memcmp(buff, buff+sizeof(uint64_t), size-sizeof(uint64_t));
    }
    
    int func4( char *buff, size_t size ){
      return *(wchar_t*)buff || wmemcmp((wchar_t*)buff, (wchar_t*)buff+1, size/sizeof(wchar_t)-1);
    }
    
    int main(){
      MEASURE( func1 );
      MEASURE( func2 );
      MEASURE( func3 );
      MEASURE( func4 );
    }
    

    我的旧电脑上的结果:

    func1: zero         108668
    func2: zero          38680
    func3: zero           8504
    func4: zero          24768

    【讨论】:

    • 很有趣,但是 func3 和 func4 需要一些小的修复来解决非整数倍的字长和对齐问题
    • 请参阅下面的@dissasorted 的答案以观察此基准测试:stackoverflow.com/a/29382742/1185900
    • 这可能会在未映射页面之前的 1 字节缓冲区出现故障。但是,是的,利用 asm 优化库函数的有趣想法。检查 16 或 32 字节偏移量对于大输入可能更好,因此库函数不需要执行未对齐的 SIMD 向量。 但是,*(uint64_t*)buff 违反了严格的别名 - 您需要使用 memcpy 加载它或声明 GNU C may_alias 版本的类型:Why does glibc's strlen need to be so complicated to run quickly?
    • 请参阅How to get the CPU cycle count in x86_64 from C++?"=A" 不会在 64 位代码中为 rdtsc 执行您想要的操作。还要注意 CPU 频率的预热效应:先慢后快。
    【解决方案5】:

    如果您的程序是仅 x86 或仅 x64,您可以使用内联 assambler 轻松优化。 REPE SCASD 指令将扫描一个缓冲区,直到找到一个非 EAX dword。

    由于没有等效的标准库函数,因此没有编译器/优化器可能能够使用这些指令(如 Sufian 的代码所证实的那样)。

    从头开始,如果您的缓冲区长度是 4 字节对齐(MASM 语法),则可以这样做:

    _asm {
       CLD                ; search forward
       XOR EAX, EAX       ; search for non-zero
       LEA EDI, [buf]     ; search in buf
       MOV ECX, [buflen]  ; search buflen bytes
       SHR ECX, 2         ; using dwords so len/=4
       REPE SCASD         ; perform scan
       JCXZ bufferEmpty:  ; completes? then buffer is 0
    }
    

    托马斯

    编辑:更新了 Tony D 的修复程序

    【讨论】:

    • 最佳答案 - 赞成。你能告诉我如何将它作为嵌入式 ASM 放入 GCC/Clang C 程序中吗? (这需要 GAS 语法吗?)是否有使用 unsigned long long 的 64 位版本?
    • +1 可靠方法 - 最佳选择:应该是 LEA EDI, ... 而不是 ESI,并且缺少 CLD 以保证方向标志清晰。
    • 小心的内联汇编器不能被编译器静态分析,并导致块前后的完整排序内存栅栏!在某些情况下,这可能很可怕。
    • 不幸的是,repe scasdisn't faster than a normal loop。字符串存储和复制指令(rep stosrep movs)在 Intel CPU 中优化了微码实现,但 repe scasrepe cmps 没有。 @TonyD:所有正常的 ABI 都需要在函数入口时清除方向标志。 cld 是在浪费一条指令。
    【解决方案6】:

    对于这么简单的事情,你需要看看编译器生成了什么代码。

    $ gcc -S -O3 -o empty.s empty.c
    

    以及程序集的内容:

            .text
            .align 4,0x90
    .globl _is_empty
    _is_empty:
            pushl       %ebp
            movl        %esp, %ebp
            movl        12(%ebp), %edx  ; edx = pointer to buffer
            movl        8(%ebp), %ecx   ; ecx = size
            testl       %edx, %edx
            jle L3
            xorl        %eax, %eax
            cmpb        $0, (%ecx)
            jne L5
            .align 4,0x90
    L6:
            incl        %eax            ; real guts of the loop are in here
            cmpl        %eax, %edx
            je  L3
            cmpb        $0, (%ecx,%eax) ; compare byte-by-byte of buffer
            je  L6
    L5:
            leave
            xorl        %eax, %eax
            ret
            .align 4,0x90
    L3:
            leave
            movl        $1, %eax
            ret
            .subsections_via_symbols
    

    这是非常优化的。循环做了三件事:

    • 增加偏移量
    • 将偏移量与大小进行比较
    • 比较内存中base+offset与0的字节数据

    可以通过逐字比较来稍微优化,但是你需要担心对齐等问题。

    当所有其他方法都失败时,先衡量,不要猜测。

    【讨论】:

    • 使用字符串指令REP SCASx可以进一步优化
    • @Tomas:仅凭借repe scasd 一次检查多个字节。另外,这个asm有点烂。它使用 2 寄存器寻址模式,但仍然进行比较和分支,而不是使用 inc 设置的标志。更好的办法是获取指向数组末尾的指针并将负索引向上计数为零。另外,Sufian:一次做一个词或双词将是一个巨大的优化,而不仅仅是轻微的。大缓冲区的两倍(word)或四(dword)因子。此外,现代 Intel CPU apparently can't micro-fuse 2-reg addressing modes
    【解决方案7】:

    在可能的情况下尝试使用 int 大小的变量检查缓冲区(应该对齐)。

    在我的脑海中(未编译、未经测试的代码如下 - 几乎可以肯定这里至少存在一个错误。这只是给出了一般的想法):

    /* check the start of the buf byte by byte while it's unaligned */
    while (size && !int_aligned( buf)) {
        if (*buf != 0) {
            return 0;
        }
    
        ++buf;
        --size;
    }
    
    
    /* check the bulk of the buf int by int while it's aligned */
    
    size_t n_ints = size / sizeof( int);
    size_t rem = size / sizeof( int);
    
    int* pInts = (int*) buf;
    
    while (n_ints) {
        if (*pInt != 0) {
            return 0;
        }
    
        ++pInt;
        --n_ints;
    }
    
    
    /* now wrap up the remaining unaligned part of the buf byte by byte */
    
    buf = (char*) pInts;
    
    while (rem) {
        if (*buf != 0) {
            return 0;
        }
    
        ++buf;
        --rem;
    }
    
    return 1;
    

    【讨论】:

      【解决方案8】:

      使用 x86,您可以使用 SSE 一次测试 16 个字节:

      #include "smmintrin.h" // note: requires SSE 4.1
      
      int is_empty(const char *buf, const size_t size) 
      {
          size_t i;
          for (i = 0; i + 16 <= size; i += 16)
          {
              __m128i v = _mm_loadu_si128((m128i *)&buf[i]);
              if (!_mm_testz_si128(v, v))
                  return 0;
          }
          for ( ; i < size; ++i)
          {
              if (buf[i] != 0)
                  return 0;
          }
          return 1;
      }
      

      这可能会通过循环展开进一步改进。

      在带有 AVX 的现代 x86 CPU 上,您甚至可以使用 256 位 SIMD 并一次测试 32 个字节。

      【讨论】:

      • 您可能会看到一次测试 64B(一个缓存行)的小改进,方法是将它们组合在一起并在结果上使用 ptest。 (ptest 是 2 微秒,并且不会与分支进行宏融合)。每次迭代使用多个por 指令应该有助于使两个加载端口饱和,因为存在循环开销。另请注意,如果 size >= 16,则清理循环可以是在最后一个字节处结束的未对齐加载的单个 ptest。这个负载与最后一个主循环迭代重叠是可以的,因为零测试是幂等的。
      • @PaulR:我的错误,未对齐的负载是可以的。对于第一批未对齐的负载,您可能会获得更好的性能,然后对齐指针并为数组的其余部分使用对齐的负载。
      • @chqrlie:你说的是真的,但在现代 CPU 上,使用未对齐负载时整体影响很小。在可能的情况下确保对齐仍然值得,但未对齐负载的损失远没有在旧 CPU 上那么显着。
      【解决方案9】:

      Hackers Delight 书籍/网站都是关于优化的 C/汇编的。该网站也提供了很多很好的参考资料,并且是最新的(AMD64、NUMA 技术)。

      【讨论】:

        【解决方案10】:

        查看fast memcpy - 它可以适用于 memcmp(或 memcmp 针对一个常量值)。

        【讨论】:

        • 这里的内容太少了,无法回答...这应该只是一个评论,或者充实如何适应。
        【解决方案11】:

        我看到很多人说对齐问题会阻止您进行字长访问,但这并不总是正确的。如果您正在寻找可移植的代码,那么这肯定是一个问题,但是 x86 实际上会容忍未对齐的访问。例如,如果在 EFLAGS 中打开对齐检查,这只会在 x86 上失败(当然 buf 实际上不是字对齐的)。

        int is_empty(char * buf, int size) {
         int i;
         for(i = 0; i < size; i+= 4) {
           if(*(int *)(buf + i) != 0) {
             return 0;
           }   
         }
        
         for(; i < size; i++) {
           if(buf[i] != 0) 
             return 0;
         }
        
         return 1;
        }
        

        尽管编译器可以将您的原始循环转换为基于单词的比较循环,并带有额外的跳转来处理对齐问题,但是它不会在任何正常的优化级别上执行此操作,因为它缺少信息。对于大小较小的情况,以这种方式展开循环会使代码变慢,并且编译器希望保持保守。

        解决此问题的一种方法是使用配置文件引导优化。如果您让 GCC 获取有关 is_empty 函数的配置文件信息,然后重新编译它,它将愿意将循环展开为带有对齐检查的字大小比较。您也可以使用 -funroll-all-loops 强制执行此行为

        【讨论】:

          【解决方案12】:

          有没有人提到展开循环?在任何这些循环中,循环开销和索引都会很重要。

          另外,缓冲区实际为空的概率是多少?这是您必须检查所有内容的唯一情况。 如果缓冲区中通常有一些垃圾,循环应该很早就停止,所以没关系。

          如果您打算将其清零(如果它不为零),那么使用memset(buf, 0, sizeof(buf)) 将其清零可能会更快,无论它是否已经为零。

          【讨论】:

            【解决方案13】:

            如何从大小循环到零(更便宜的检查):

            int is_empty(char * buf, int size) 
            {
                while(size --> 0) {
                    if(buf[i] != 0) return 0;
                }
                return 1;
            }
            

            必须注意,我们可能无法超越编译器,因此请在您的编译器中启用最激进的速度优化,并假设您可能不会更快。

            或使用指针处理所有事情(未经测试,但可能表现相当不错):

            int is_empty(char* buf, int size)
            {
                char* org = buf;
            
                if (buf[size-1] == 1)
                    return 0;
            
                buf[size-1] = 1;
                while(! *buf++);
                buf--;
            
                return buf == org[size-1];
            }
            

            【讨论】:

            • 我同意你的第一句话(循环到零)。另一个微不足道的优化是删除索引的使用,即将 if 语句替换为 if (*buf++) return 0;
            • 你的第二种方法很有趣!当然,它只有在允许写入缓冲区时才有效(通常情况并非如此)。并且代码中存在一些bug:size == 0时越界访问,buf[size-1]的原始内容没有恢复,最后一条语句漏掉了一个&amp;。我也会避免对buf[size-1] 进行不必要的地址计算(三次)。但我喜欢开箱即用的思维方式,在某些情况下这个解决方案相对简单高效:)
            【解决方案14】:

            您在问题中表示您正在寻找最有可能不必要的微优化。在“正常”情况下,Thomas 和其他人的 ASM 方法应该会给您最快的结果。

            不过,这还是忘记了大局。如果您的缓冲区真的很大,那么从一开始就必须进行线性搜索绝对不是最快的方法。假设您的 cp 替换非常擅长查找大的连续空区域,但在数组末尾有一些非空字节。所有线性搜索都需要读取整个数组。另一方面,受快速排序启发的算法可以搜索任何非零元素,并为足够大的数据集更快地中止。

            因此,在进行任何类型的微优化之前,我会仔细查看缓冲区中的数据,看看是否有任何模式。对于单个“1”,随机分布在缓冲区中,线性搜索(忽略线程/并行化)将是最快的方法,在其他情况下不一定如此。

            【讨论】:

              【解决方案15】:

              初始 C 代码的内联汇编版本(没有错误检查,如果 uiSize== 0 和/或数组未分配将生成异常。也许使用 try {} catch() 因为这可能比添加对代码进行大量检查。或者像我一样,尽量不要调用具有无效值的函数(通常不起作用)。至少添加一个NULL指针检查和一个size != 0检查,这很容易。

               unsigned int IsEmpty(char* pchBuffer, unsigned int uiSize)
               {
                  asm {
                    push esi
                    push ecx         
              
                    mov esi, [pchBuffer]
                    mov ecx, [uiSize]
              
                    // add NULL ptr and size check here
              
                    mov eax, 0
              
                  next_char:
                    repe scasb           // repeat string instruction as long as BYTE ptr ds:[ESI] == 0
                                         // scasb does pointer arithmetic for BYTES (chars), ie it copies a byte to al and increments ESI by 1
                    cmp cx,0             // did the loop complete?
                    je all_chars_zero    // yes, array is all 0
                    jmp char_not_zero    // no, loop was interrupted due to BYTE PTR ds:[ESI] != 0
              
                  all_chars_zero:        
                    mov eax, 1           // Set return value (works in MASM)
                    jmp end  
              
                  char_not_zero:
                    mov eax, 0          // Still not sure if this works in inline asm
              
                  end:
                    pop ecx
                    pop esi          
                }
              }
              

              这是即时写的,但看起来很正确,欢迎更正。如果有人知道如何从内联 asm 设置返回值,请告诉。

              【讨论】:

              • repe scasb 在任何现代 x86 CPU 上都不是很快。 agner.org/optimize。此外,repe scasb 保留标志设置(除非 ECX 为零,在这种情况下它为零 scasb)。您只需要一个没有分支的setz al。或者,如果您确实查看 ECX,请查看整个 ECX,而不仅仅是 CX。您也不需要自己推送/弹出注册表;如果需要,编译器将在整个函数周围保存/恢复它们。 (ECX 不需要;它已被调用。)
              • @PeterCordes,rep 前缀与其他方法的比较?
              • 使用 AVX vmovaps + vorps + vptest + jnz 在 Haswell / Skylake 或 Zen 2 上以每个时钟周期大约 64 字节的速度检查 64 字节缓存线。(可能需要为 128 字节块展开更多操作,以避免实际出现前端瓶颈)。使用rep scasq,您可以检查 8 个字节/时钟。 rep scasb 可以在 Skylake 等现代 Intel 上每时钟检查 1 个字节。
              【解决方案16】:
              int is_empty(char * buf, int size) 
              {
                  int i, content=0;  
                  for(i = 0; !content && i < size; i++)    
                  {  
                      content=content | buf(i);       // bitwise or  
                  }  
                  return (content==0);  
              }
              

              【讨论】:

              • a) 请使用格式。 b) || 不是按位或,而是逻辑或。
              • c) 为提高效率,请将for() 循环更改为for(i = 0; !content &amp;&amp; i &lt; size; i++),这样一旦发现不良内容就不会继续循环。
              • 如果您要在每次迭代中检查content,那么对它进行按位或运算是没有意义的。对整个对齐的数据块进行按位或运算可以减少分支,或者让编译器优化为一次检查 4 或 8 个字节的 asm,但这没有用。
              【解决方案17】:
              int is_empty(char * buf, int size)
              {
                 return buf[0] == '\0';
              }
              

              如果您的缓冲区不是字符串,我认为这是检查的最快方法...

              memcmp() 将要求您创建一个相同大小的缓冲区,然后使用 memset 将其全部设置为 0。我怀疑这会更快...

              【讨论】:

              • memcmp() 并不需要。您可以使用{ 0 } 初始化一个非常大的静态缓冲区,并仅将其中的一部分与相关缓冲区进行比较。
              • 一个非常大的全零缓冲区如果在操作系统的新页面开始时复制的系统上使用calloc 分配它会更好地执行很多写入映射到相同的物理零页。这样整个calloc 缓冲区在L1 缓存中是热的。不过,您仍然会错过 TLB。 memcmp 值得考虑的唯一原因是,当前的编译器真的很讨厌编译找到某些东西后退出的循环。它们不会自动矢量化,并且一次字节循环通常保持一次字节。不过,您可以通过 ORing 一些 size_ts 来获得好的结果。
              【解决方案18】:

              编辑:错误答案

              一种新颖的方法可能是

              int is_empty(char * buf, int size) {
                  char start = buf[0];
                  char end = buff[size-1];
                  buf[0] = 'x';
                  buf[size-1] = '\0';
                  int result = strlen(buf) == 0;
                  buf[0] = start;
                  buff[size-1] = end;
                  return result;
              }
              

              为什么这么疯狂?因为 strlen 是最有可能被优化的库函数之一。 存储和替换第一个字符是为了防止误报。存储和替换最后一个字符是为了确保它终止。

              【讨论】:

              • 这不适用于二进制数据0 0 1 0 等。它不检查字符串是否为空。
              【解决方案19】:

              最初的 C 算法几乎和 VALID C 中的一样慢。 如果您坚持使用 C,那么请尝试使用“while”循环而不是“for”:

                   int i = 0;
                   while (i< MAX)
                   {
                      // operate on the string
                      i++;
                   }
              

              这几乎是您可以用 C 编写的最快的一维字符串操作循环,此外,如果您可以强制编译器使用“register”关键字将 i 放入寄存器中,但我被告知这几乎总是被忽略现代编译器。

              同样搜索一个固定大小的数组来判断是否为空非常浪费,而且0也不是空的,它是数组中的值。

              一个更好的速度解决方案是使用一个动态数组(int* piBuffer)和一个存储当前大小的变量(unsigned int uiBufferSize),当数组为空时指针为NULL,uiBufferSize为0。一个将这两个作为受保护成员变量的类。还可以轻松地为动态数组编写一个模板,该模板将存储 32 位值,原始类型或指针,对于原始类型,实际上没有任何方法可以测试“空”(我将其解释为“未定义”),但是您当然可以定义 0 来表示可用条目。对于数组指针,您应该将所有条目初始化为 NULL,并在刚刚释放该内存时将条目设置为 NULL。 NULL 确实意味着“什么都没有”,所以这是表示空的非常方便的方式。不应该在真正复杂的算法中使用动态调整大小的数组,至少在开发阶段不应该,有太多事情可能出错。至少应该首先使用 STL 容器(或经过良好测试的替代方案)实现算法,然后当代码工作时,可以将测试容器交换为简单的动态数组(如果您可以避免过于频繁地调整数组大小,则代码将更快,更安全。

              对于复杂而酷的代码,一个更好的解决方案是根据您的需要使用 std::vector 或 std::map(或任何容器类 STL、本土或第 3 方),但查看您的代码我会说std::vector 就足够了。 STL 容器是模板,所以它们也应该很快。使用 STL Container 来存储对象指针(始终存储对象指针而不是实际对象,为每个条目复制整个对象会真正搞乱您的执行速度)和用于更多基本数据(位图、声音等)的动态数组,即原始类型。一般。

              我是通过学习x86汇编语言手册独立提出REPE SCASW方案的,我同意使用这个字符串操作指令的例子是最快的。另一个具有单独比较、跳转等指令的汇编示例几乎肯定要慢得多(但仍然比初始 C 代码快得多,所以仍然是一篇好文章),因为字符串操作是所有现代 CPU 上优化程度最高的操作之一,他们甚至可能有自己的逻辑电路(有人知道吗?)。

              REPE SCASD 不需要获取新指令,也不需要增加指令指针,这只是像我这样的汇编新手可以想出的东西,最重要的是硬件优化,字符串操作很关键对于几乎所有类型的现代软件,特别是多媒体应用程序(复制 PCM 声音数据、未压缩的位图数据等),因此每次设计新的 80x86 芯片时,优化这些指令必须是非常高的优先级。 我将它用于一种新颖的 2d 精灵碰撞算法。

              它说我不能发表意见,因此请考虑以下客观评估:现代编译器(UNMANAGED C/C++,几乎所有其他内容都是托管代码,而且速度非常慢)非常擅长优化,但无法避免的是,对于非常具体的任务,编译器会生成冗余代码。可以查看编译器输出的程序集,这样就不必完全从头开始翻译复杂的算法,尽管这样做(对某些人来说)很有趣,而且用困难的方式编写代码更有价值,但是无论如何,使用“for”循环的算法,特别是关于字符串操作,通常可以非常显着地优化,因为 for 循环会生成大量代码,这通常是不需要的,例如: for (int i = 1000; i>0; i--) DoSomething();如果编译器不是很聪明(可能是),这行会生成 6-10 行汇编,但优化的汇编版本可以是:

                    mov cx, 1000
                  _DoSomething:
                    // loop code....or call Func, slower but more readable
                    loop _DoSomething
              

              那是 2 行,它与 C 行完全相同(它使用寄存器而不是内存地址,这要快得多,但可以说这与 C 行不完全相同,但这是语义) ,这个例子的优化程度取决于现代编译器的优化程度,我对此一无所知,但是基于以最少和更快的装配线实现算法为目标的算法分析通常效果很好,我有非常好的结果,首先在 C/C++ 中实现算法而不关心优化,然后在汇编中翻译和优化它。每条 C 行变成多条流水线的事实往往使得一些优化非常明显,而且一些指令比其他指令更快:

                    INC DX ; is faster than:
                    ADD DX,1 ;if ADD DX,1 is not just replaced with INC DX by the assembler or the CPU
                    LOOP ; is faster than manually decreasing, comparing and jumping
                    REPxx STOSx/MOVSx/LODSx is faster than using cmp, je/jne/jea etc and loop
                    JMP or conditional jumping is faster than using CALL, so in a loop that is executed VERY frequently (like rendering), including functions in the code so it is accessible with "local" jumps can also boost performance.
              

              最后一点与这个问题非常相关,快速字符串操作。 所以这篇文章并不是漫无边际的。

              最后,以在典型执行中需要最少跳转次数的方式设计汇编算法。

              也不要费心优化不经常调用的代码,使用分析器查看最常调用的代码,然后从它开始,任何被调用少于每秒 20 次的代码(完成速度比1000 ms/20) 真的不值得优化。查看未与计时器等同步并在完成后立即再次执行的代码。另一方面,如果你的渲染循环可以在一台普通机器上完成 100+ FPS,那么优化它在经济上没有意义,但是真正的编码人员喜欢编码而不关心经济性,他们将 AppStart() 方法优化为 100 % 组装,即使它只被调用一次 :) 或者使用 az 旋转矩阵将俄罗斯方块旋转 90 度 :P 任何这样做的人都很棒!

              如果有人有一些建设性的更正,这不是很伤人,那么我很想听听,我几乎完全自己编码,所以我并没有真正受到任何影响。我曾经花钱请一位优秀的加拿大游戏开发人员教我的 Direct3d,虽然我可以轻松地读一本书,但与另一位在某些领域比我水平略高的程序员互动很有趣。

              总体而言,感谢您提供的优质内容。我想我会去回答一些简单的问题,回馈一下。

              【讨论】:

              • 不要在 asm 中使用 16 位操作数大小。并且永远不要使用loop 指令,除非您以牺牲速度为代价来优化代码大小。此外,repe scasd 并不比普通循环快。没有理由使用repe scasw:如果您要处理更大的块(对齐、读取结束等),那么使用 4B 块(或 64 位模式下的 8B)。或带有 SSE2 向量指令的 16B。见agner.org/optimize
              • 另外,for 循环的编译方式与 while 循环不同。 register 关键字(几乎?)对 gcc 没有任何作用。它已经擅长尽可能多地保存在寄存器中,最大限度地减少内存溢出。您对 OP 代码“与有效 C 一样慢”的评论是完全错误的。我觉得很好。我没有将它插入gcc.godbolt.org,但我希望它的while 版本能够编译为相同的asm 指令。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2016-05-24
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多