将C int数组重置为零:最快的方法?

Vin*_*ent 92 c c++ arrays memset

假设我们有一个T myarray[100]带有T = int,unsigned int,long long int或unsigned long long int,将所有内容重置为零的最快方法是什么(不仅用于初始化,而且在我的程序中多次重置内容) ?也许有memset?

像动态数组一样的问题T *myarray = new T[100].

Mat*_*lia 155

memset(from <string.h>)可能是最快的标准方式,因为它通常是直接在装配中编写并手工优化的例程.

memset(myarray, 0, sizeof(myarray)); // for automatically-allocated arrays
memset(myarray, 0, N*sizeof(*myarray)); // for heap-allocated arrays, where N is the number of elements
Run Code Online (Sandbox Code Playgroud)

顺便说一下,在C++中,惯用的方法是使用std::fill(from <algorithm>):

std::fill(myarray, myarray+N, 0);
Run Code Online (Sandbox Code Playgroud)

可以被自动优化成memset; 我敢肯定,这将工作以最快的速度memsetintS,虽然它可能对较小的类型,如果优化器是不够聪明执行略差.不过,如果有疑问,请介绍一下.

  • 从1999年的ISO C标准来看,实际上并没有保证`memset`会将整数设置为0; 没有具体说明all-bits-zero是'0`的表示.技术勘误增加了此类保证,该保证包含在2011 ISO C标准中.我相信all-bits-zero*是所有现有C和C++实现中所有整数类型的'0`的有效表示,这就是委员会能够添加该要求的原因.(浮点或指针类型没有类似的保证.) (9认同)
  • 添加到@ KeithThompson的评论:此保证在TC2(2004)的纯文本中添加到6.2.6.2/5; 但是如果没有填充位,那么6.2.6.2/1和/ 2已经保证所有位零为"0".(对于填充比特,存在所有比特零可能是陷阱表示的可能性).但无论如何,TC应该承认并替换有缺陷的文本,所以到2004年我们应该表现得好像C99总是包含这个文本. (3认同)

Ben*_*min 16

这个问题,虽然相当陈旧,但需要一些基准,因为它要求的不是最惯用的方式,或者可以用最少的行数写出的方式,而是最快的方式.没有一些实际的测试,回答这个问题是愚蠢的.所以我比较了四个解决方案,memset与std :: fill对比AnT的答案与使用AVX内在函数的解决方案的ZERO.

请注意,此解决方案不是通用的,它仅适用于32位或64位的数据.如果此代码执行错误,请评论.

#include<immintrin.h>
#define intrin_ZERO(a,n){\
size_t x = 0;\
const size_t inc = 32 / sizeof(*(a));/*size of 256 bit register over size of variable*/\
for (;x < n-inc;x+=inc)\
    _mm256_storeu_ps((float *)((a)+x),_mm256_setzero_ps());\
if(4 == sizeof(*(a))){\
    switch(n-x){\
    case 3:\
        (a)[x] = 0;x++;\
    case 2:\
        _mm_storeu_ps((float *)((a)+x),_mm_setzero_ps());break;\
    case 1:\
        (a)[x] = 0;\
        break;\
    case 0:\
        break;\
    };\
}\
else if(8 == sizeof(*(a))){\
switch(n-x){\
    case 7:\
        (a)[x] = 0;x++;\
    case 6:\
        (a)[x] = 0;x++;\
    case 5:\
        (a)[x] = 0;x++;\
    case 4:\
        _mm_storeu_ps((float *)((a)+x),_mm_setzero_ps());break;\
    case 3:\
        (a)[x] = 0;x++;\
    case 2:\
        ((long long *)(a))[x] = 0;break;\
    case 1:\
        (a)[x] = 0;\
        break;\
    case 0:\
        break;\
};\
}\
}
Run Code Online (Sandbox Code Playgroud)

我不会声称这是最快的方法,因为我不是一个低级别的优化专家.相反,它是一个正确的架构相关实现的例子,比memset更快.

现在,进入结果.我计算了100个int和long long数组的性能,无论是静态还是动态分配,但除了msvc之外,它在静态数组上消除了死代码,结果非常可比,所以我只展示动态数组性能.使用time.h的低精度时钟功能,时间标记为100万次迭代.

clang 3.8(使用clang-cl前端,优化标志=/OX/arch:AVX/Oi/Ot)

int:
memset:      99
fill:        97
ZERO:        98
intrin_ZERO: 90

long long:
memset:      285
fill:        286
ZERO:        285
intrin_ZERO: 188
Run Code Online (Sandbox Code Playgroud)

gcc 5.1.0(优化标志:-O3 -march = native -mtune = native -mavx):

int:
memset:      268
fill:        268
ZERO:        268
intrin_ZERO: 91
long long:
memset:      402
fill:        399
ZERO:        400
intrin_ZERO: 185
Run Code Online (Sandbox Code Playgroud)

msvc 2015(优化标志:/ OX/arch:AVX/Oi/Ot):

int
memset:      196
fill:        613
ZERO:        221
intrin_ZERO: 95
long long:
memset:      273
fill:        559
ZERO:        376
intrin_ZERO: 188
Run Code Online (Sandbox Code Playgroud)

这里有很多有趣的内容:llvm杀死gcc,MSVC的典型参差优化(它在静态数组上消除了令人印象深刻的死代码,然后填充性能很差).虽然我的实现速度明显更快,但这可能只是因为它认识到位清除的开销比任何其他设置操作都少得多.

Clang的实现值得关注,因为它的速度要快得多.一些额外的测试表明,它的memset实际上专门用于零 - 非零存储器,400字节阵列要慢得多(~220ms)并且与gcc相当.但是,使用800字节数组的非零memset不会产生速度差异,这可能就是为什么在这种情况下,它们的memset性能比我的实现更差 - 专门化仅适用于小型数组,而截止时间大约为800字节.另请注意,gcc'fill'和'ZERO'没有优化到memset(查看生成的代码),gcc只是生成具有相同性能特征的代码.

结论:memset并没有真正优化这个任务,人们会假装它(否则gcc和msvc和llvm的memset将具有相同的性能).如果性能很重要,那么memset不应该是最终解决方案,特别是对于这些笨拙的中型阵列,因为它不专门用于位清除,并且它没有比编译器自己做的更优化.

  • 没有代码的基准测试,没有提到编译器版本和使用的选项?嗯... (4认同)
  • @MottiShneor 它看起来比实际情况更复杂。AVX 寄存器的大小为 32 字节。所以他计算寄存器中有多少个“a”值。然后,他循环遍历所有 32 字节块,这些块应该使用指针算术完全覆盖(“(float *)((a)+x)”)。这两个内在函数(以`_mm256`开头)只是创建一个零初始化的32字节寄存器并将其存储到当前指针。这是前 3 行。其余部分仅处理最后 32 字节块不应被完全覆盖的所有特殊情况。由于矢量化,速度更快。- 我希望这有帮助。 (4认同)

Ale*_*lds 11

来自memset():

memset(myarray, 0, sizeof(myarray));
Run Code Online (Sandbox Code Playgroud)

sizeof(myarray)如果myarray在编译时已知大小,则可以使用.否则,如果您使用动态大小的数组,例如通过malloc或获得new,则需要跟踪长度.

  • 即使在编译时不知道数组的大小,sizeof也能工作.(当然,只有当它的阵列时) (2认同)
  • @asaelr:在C++中,`sizeof`总是在编译时进行评估(不能与VLA一起使用).在C99中,对于VLA,它可以是运行时表达式. (2认同)
  • @asaelr:在C++中,他是完全正确的.您的评论没有说明C99或VLA,所以我想澄清一下. (2认同)

AnT*_*AnT 5

您可以使用memset,但仅仅因为我们选择的类型仅限于整数类型.

在C的一般情况下,实现宏是有意义的

#define ZERO_ANY(T, a, n) do{\
   T *a_ = (a);\
   size_t n_ = (n);\
   for (; n_ > 0; --n_, ++a_)\
     *a_ = (T) { 0 };\
} while (0)
Run Code Online (Sandbox Code Playgroud)

这将为您提供类似C++的功能,可以让您"重置为0"任意类型的对象数组,而不必诉诸于黑客memset.基本上,这是C++函数模板的C模拟,除了你必须明确指定类型参数.

最重要的是,您可以为非衰减数组构建"模板"

#define ARRAY_SIZE(a) (sizeof (a) / sizeof *(a))
#define ZERO_ANY_A(T, a) ZERO_ANY(T, (a), ARRAY_SIZE(a))
Run Code Online (Sandbox Code Playgroud)

在您的示例中,它将应用为

int a[100];

ZERO_ANY(int, a, 100);
// or
ZERO_ANY_A(int, a);
Run Code Online (Sandbox Code Playgroud)

值得注意的是,特别是对于标量类型的对象,可以实现与类型无关的宏

#define ZERO(a, n) do{\
   size_t i_ = 0, n_ = (n);\
   for (; i_ < n_; ++i_)\
     (a)[i_] = 0;\
} while (0)
Run Code Online (Sandbox Code Playgroud)

#define ZERO_A(a) ZERO((a), ARRAY_SIZE(a))
Run Code Online (Sandbox Code Playgroud)

把上面的例子变成了

 int a[100];

 ZERO(a, 100);
 // or
 ZERO_A(a);
Run Code Online (Sandbox Code Playgroud)