【问题标题】:Fast way to replace elements in array - C替换数组中元素的快速方法-C
【发布时间】:2013-04-20 07:31:09
【问题描述】:

假设我们有一个这样的整数数组:

const int size = 100000;
int array[size];
//set some items to 0 and other items to 1

我想将所有值为 1 的项目替换为另一个值,例如 123456。 这可以通过以下方式轻松实现:

for(int i = 0; i < size ; i++){
    if(array[i] != 0) 
        array[i] = 123456;
}

出于好奇,是否有更快的方法来做到这一点,通过某种 x86 技巧,或者这是处理器的最佳代码?

【问题讨论】:

  • @SuvP:这取决于所使用的编译器和平台,通常不正确。
  • @SuvP 如果数组在内部是这样翻译的,那你写它的方式有什么关系呢?
  • @SuvP 现代处理器可以使用第二个寄存器给出的偏移量进行寻址,这就是现在任何体面的编译器如何实现array[i]。通常不会发生明确的算术运算。
  • 您是否确认(通过分析或其他方式)在您的特定情况下,琐碎的实现是实际的性能瓶颈? (我承认;我最近一直在研究相对性能关键的代码,像这样的优化实际上可能会产生真正的影响。我做的第一件事就是编写一个小工具,让我可以对各种替代实现。通过分析器运行它会指出我一开始甚至没有想到需要优化的部分代码,通过一些小改动,我提高了大约 30% 的性能。)
  • @SuvP:直接指针查找比数组查找更快,因为 p 只是一个解引用,而 arr[1] 需要从 arr 偏移,而 *then取消引用。 *(arr + 1) 并不快,你只是在移动偏移计算。正如 JensGustedt 所说,现代处理器为这种非常常见的动作提供了硬件支持,这使得这种解释过于简单化了。在任何情况下,除非您有充分的理由,否则不要执行 *(arr+offset),因为如果它更快,那么编译器会将您的数组查找重写为。

标签: c arrays performance


【解决方案1】:

对于您最初具有 0 和 1 的特定情况,以下 可能会更快。您必须对其进行基准测试。不过,使用纯 C 语言可能不会做得更好。如果您想利用可能存在的“x86 诡计”,您可能需要深入组装。

for(int i = 0; i < size ; i++){
  array[i] *= 123456;
}

编辑:

基准代码:

#include <time.h>
#include <stdlib.h>
#include <stdio.h>

size_t diff(struct timespec *start, struct timespec *end)
{
  return (end->tv_sec - start->tv_sec)*1000000000 + end->tv_nsec - start->tv_nsec;
}

int main(void)
{
  const size_t size = 1000000;
  int array[size];

  for(size_t i=0; i<size; ++i) {
    array[i] = rand() & 1;
  }

  struct timespec start, stop;

  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
  for(size_t i=0; i<size; ++i) {
    array[i] *= 123456;
    //if(array[i]) array[i] = 123456;
  }
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &stop);

  printf("size: %zu\t nsec: %09zu\n", size, diff(&start, &stop));
}

我的结果:

计算机:四核 AMD Phenom @2.5GHz,Linux,GCC 4.7,编译为

$ gcc arr.c -std=gnu99 -lrt -O3 -march=native
  • if 版本:~5-10ms
  • *= 版本:~1.3ms

【讨论】:

  • +1 用于基准测试建议 - 这是所有性能相关问题的“必备”
  • 哼,不,乘法方式比比较+加法慢。所以我猜你并没有节省时间。当然它更短,但它会更慢。
  • @xgbi 整数乘以 1 或 0 实际上非常快,并且内部循环是无分支的。尽管如此,它仍需要进行基准测试。
  • 我认为如果你使用if,你在branch prediction中基本上会失败,这就是为什么乘法会更快。
  • -array[i] &amp; 123456 可能会稍微快一点。
【解决方案2】:

对于像你这样的小数组,尝试寻找另一种算法是没有用的,如果值不是特定模式,那么简单的循环是唯一的方法。

但是,如果您有一个非常大的数组(我们说的是几百万个条目),那么您可以将工作拆分为线程。每个单独的线程处理整个数据集的一小部分。

【讨论】:

    【解决方案3】:

    您可能还想对此进行基准测试:

    for(int i = 0; i < size ; i++){
      array[i] = (~(array[i]-1) & 123456);
    }
    

    我通过与 SchighSchagh 相同的基准运行它,我的设置几乎没有差异。但是,您的可能会有所不同。

    编辑:停止印刷机!

    我只记得如果“:”之间的参数是常量,x86 可以“取消分支”三元运算符。考虑以下代码:

    for(size_t i=0; i<size; ++i) {
        array[i] = array[i] ? 123456 : 0;
    }
    

    看起来几乎就像您的原始代码,不是吗?嗯,反汇编显示已经编译,没有任何分支:

      for(size_t i=0; i<size; ++i) {
    00E3104C  xor         eax,eax  
    00E3104E  mov         edi,edi  
            array[i] = array[i] ? 123456 : 0;
    00E31050  mov         edx,dword ptr [esi+eax*4]  
    00E31053  neg         edx  
    00E31055  sbb         edx,edx  
    00E31057  and         edx,1E240h  
    00E3105D  mov         dword ptr [esi+eax*4],edx  
    00E31060  inc         eax  
    00E31061  cmp         eax,5F5E100h  
    00E31066  jb          wmain+50h (0E31050h)  
        }
    

    在性能方面,它似乎与我原来的 SchighSchagh 解决方案相当或略胜一筹。不过,它更具可读性和灵活性。例如,它可以使用值不同于 0 和 1 的 array[i]。

    底线,基准测试并查看反汇编。

    【讨论】:

      【解决方案4】:

      数组足够小,可以放入缓存,因此值得使用 SIMD:(未测试)

        mov ecx, size
        lea esi, [array + ecx * 4]
        neg ecx
        pxor xmm0, xmm0
        movdqa xmm1, [_vec4_123456]  ; value of { 123456, 123456, 123456, 123456 }
      _replaceloop:
        movdqa xmm2, [esi + ecx * 4] ; assumes the array is 16 aligned, make that true
        add ecx, 4
        pcmpeqd xmm2, xmm0
        pandn xmm2, xmm1
        movdqa [esi + ecx * 4 - 16], xmm2
        jnz _replaceloop
      

      展开 2 可能会有所帮助。

      如果你有 SSE4.1,你可以使用 SchighSchagh 的乘法技巧和pmulld

      【讨论】:

      • 确定这会有所帮助...openCL / 加速可能更容易维护/移植...展开 2 或更多会有所帮助...但可能不值得...跨度>
      • @GradyPlayer:使用 OpenCL,您必须将所有数据传输到 gfx 卡/加速设备,这很慢,可能比原来的建议慢得多。
      • 此操作(如果不为0,则将项目设置为123456)只需执行一次-之后每次连续操作都不会更改数据。
      【解决方案5】:

      这里有一些 Win32 代码来分析各种版本的算法(使用 VS2010 Express 使用默认发布版本编译):-

      #include <windows.h>
      #include <stdlib.h>
      #include <stdio.h>
      
      const size_t
        size = 0x1D4C00;
      
      _declspec(align(16)) int
        g_array [size];
      
      _declspec(align(16)) int
        _vec4_123456 [] = { 123456, 123456, 123456, 123456 };
      
      void Test (void (*fn) (size_t, int *), char *test)
      {
        printf ("Executing test: %s\t", test);
      
        for(size_t i=0; i<size; ++i) {
          g_array[i] = rand() & 1;
        }
      
        LARGE_INTEGER
          start,
          end;
      
        QueryPerformanceCounter (&start);
      
        fn (size, g_array);
      
        QueryPerformanceCounter (&end);
      
        printf("size: %u\t count: %09u\n", size, (int) (end.QuadPart - start.QuadPart));
      }
      
      void Test1 (size_t size, int *array)
      {
        for(size_t i=0; i<size; ++i) {
          array[i] *= 123456;
        }
      }
      
      void Test2 (size_t size, int *array)
      {
        for(size_t i=0; i<size; ++i) {
          if(array[i]) array[i] = 123456;
        }
      }
      
      void Test3 (size_t array_size, int *array)
      {
        __asm
        {
          mov edi,array
          mov ecx, array_size 
          lea esi, [edi + ecx * 4]
          neg ecx
          pxor xmm0, xmm0
          movdqa xmm1, [_vec4_123456]  ; value of { 123456, 123456, 123456, 123456 }
      _replaceloop:
          movdqa xmm2, [esi + ecx * 4] ; assumes the array is 16 aligned, make that true
          add ecx, 4
          pcmpeqd xmm2, xmm0
          pandn xmm2, xmm1
          movdqa [esi + ecx * 4 - 16], xmm2
          jnz _replaceloop
        }
      }
      
      void Test4 (size_t array_size, int *array)
      {
        array_size = array_size * 8 / 12;
      
        __asm
        {
              mov edi,array
              mov ecx,array_size
              lea esi,[edi+ecx*4]
                                            lea edi,[edi+ecx*4]
              neg ecx
                                            mov edx,[_vec4_123456]
              pxor xmm0,xmm0
              movdqa xmm1,[_vec4_123456]
      replaceloop:
              movdqa xmm2,[esi+ecx*4]
                                            mov eax,[edi]
                                            mov ebx,[edi+4]
              movdqa xmm3,[esi+ecx*4+16]
                                            add edi,16
              add ecx,9
                                            imul eax,edx    
              pcmpeqd xmm2,xmm0
                                            imul ebx,edx
              pcmpeqd xmm3,xmm0
                                            mov [edi-16],eax
                                            mov [edi-12],ebx
              pandn xmm2,xmm1
                                            mov eax,[edi-8]
                                            mov ebx,[edi-4]
              pandn xmm3,xmm1
                                            imul eax,edx    
              movdqa [esi+ecx*4-36],xmm2
                                            imul ebx,edx
              movdqa [esi+ecx*4-20],xmm3
                                            mov [edi-8],eax
                                            mov [edi-4],ebx
              loop replaceloop
        }
      }
      
      int main()
      {
          Test (Test1, "Test1 - mul");
          Test (Test2, "Test2 - branch");
          Test (Test3, "Test3 - simd");
          Test (Test4, "Test4 - simdv2");
      }
      

      用于测试:C 使用 if()...,C 使用乘法,harold 的 simd 版本和我的 simd 版本。

      多次运行(请记住,在进行分析时,您应该对多次运行的结果进行平均)所有版本之间几乎没有区别,但分支版本明显较慢。

      这并不奇怪,因为算法对每个内存项所做的工作很少。这意味着真正的限制因素是 CPU 和内存之间的带宽,CPU 一直在等待内存赶上,即使 cpu 帮助预取数据(ia32 的线性检测和预取数据)。

      【讨论】:

        【解决方案6】:

        您可以使用另一个数组或其他一些数据结构来跟踪您设置为 1 的元素的索引,然后只访问这些元素。如果只有少数元素设置为一个,这将最有效

        【讨论】:

        • 不是真的,分布可能是 50% 的零和 50% 的一
        【解决方案7】:

        这可能会更快。

        for(int i = 0; i < size ; i++){
          array[i] = ((123456 << array[i]) - 123456);
        }
        

        编辑:将按位运算更改为左移。

        【讨论】:

        • 0 &amp; 123456 == 01 &amp; 123456 == 0
        • 为你修复它:x= !!(array[i] == 0x01); y = ~x + 1;数组[i] = y&123456 + ~y&array[i];
        • 我可以确认 5 年之后,这个答案作为 x86 上最快的基准测试。我相信这种差异在 ARM 和其他一些 RISC 处理器上会更加显着,因为在它们的指令集中它们有一个时钟周期操作来执行连续的位移和加法操作
        【解决方案8】:

        您可以使用 c 内联程序集来加快数组分配的速度。如下所示,

        #include<stdio.h>
        #include<string.h>
        #include<stdlib.h>
        
        const int size = 100000; 
        void main(void) {
          int array[size];
          int value = 1000;
        
          __asm__ __volatile__("cld\n\t"
                  "rep\n\t"
                  "stosl\n\t"
                  :
                  :"c"(size*4), "a"(value), "D"(array)
                  :
                 );
        
          printf("Array[0] : %d \n", array[0]);
        }
        

        当我们与普通的 c 程序分配数组值相比时,这应该是速度。 stosl 指令也需要 4 个时钟周期。

        【讨论】:

        • -1 这甚至不是清除数组的最快方法。 (可能是最小的。)但问题在于初始化单个/随机元素。
        • 嘿伙计,你怎么说这很慢? stosl 可以将 eax 寄存器中指定的双字设置为目标指针(此处为 edi),这应该在 ecx 寄存器中指定的计数范围内起作用。完成后,当您尝试访问应该设置为 eax 寄存器的值的数组元素时。这就是内核部分实际发生的情况。
        • 真正的问题是你的程序没有解决这个问题。第二个问题是您的解决方案与所要求的语言不同。第三个问题是声称是最快的解决方案。它也不是特别慢。很抱歉不清楚。 -1 保持不变。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-06
        • 2016-08-02
        • 2023-02-10
        • 1970-01-01
        • 2012-11-15
        • 2011-03-25
        相关资源
        最近更新 更多