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


102

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

对于类似的动态数组也存在同样的问题T *myarray = new T[100]


16
@BoPersson:好吧,new C ++ ...
Matteo Italia

@Matteo-是的,是的 并没有太大影响答案(直到现在才:-)。
Bo Persson '02

3
@BoPersson:我仅memset在谈论涉及C ++时感到很沮丧... :)
Matteo Italia

2
在现代编译器上,您无法超越简单的for循环。但是,令人惊讶的是,尝试变得聪明起来,可能会做得更糟。
David Schwartz 2015年

使用结构并将数组粘贴在其中。创建一个全零的实例。使用它可以将您创建的其他人归零。它运作良好。没有包含,没有功能,非常快。
Xofo

Answers:


170

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

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

std::fill(myarray, myarray+N, 0);

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


10
从1999 ISO C标准开始,实际上并不能保证memset将整数设置为0。没有具体说明all-bits-zero是的表示0。技术勘误中添加了这样的保证,该保证包含在2011 ISO C标准中。我相信all-bits-zero 0所有现有C和C ++实现中所有整数类型的有效表示,这就是委员会能够添加该要求的原因。(对于浮点或指针类型没有类似的保证。)
Keith Thompson

3
添加到@KeithThompson的评论:TC2(2004)中以纯文本形式将此保证添加到6.2.6.2/5中;但是,如果没有填充位,则6.2.6.2/1和/ 2已经保证all-bits-zero为0。(对于填充位,存在全零可能是陷阱表示的可能性)。但是无论如何,TC应该承认并替换有缺陷的文本,因此从2004年起,我们应该像C99始终包含此文本那样行事。
MM

在C语言中,如果您正确分配了动态数组,则两个内存集之间不会有任何区别。正确的动态分配为int (*myarray)[N] = malloc(sizeof(*myarray));
隆丁

@Lundin:当然-如果您在编译时知道大小N,但是在大多数情况下,如果您使用过malloc,则只能在运行时知道。
Matteo Italia

@MatteoItalia我们自1999
Lundin

20

这个问题虽然比较老,但是却需要一些基准测试,因为它要求的不是最惯用的方式,也不是可以用最少的行数编写的方法,而是最快的方法。在没有实际测试的情况下回答这个问题是很愚蠢的。因此,我比较了四种解决方案,即AnT答案的memset与std :: fill与ZERO与我使用AVX内部函数制作的解决方案。

请注意,此解决方案不是通用的,它仅适用于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;\
};\
}\
}

我不会声称这是最快的方法,因为我不是底层优化专家。相反,它是一个正确的,依赖于体系结构的实现的示例,它比memset更快。

现在,进入结果。我计算了大小为100的int和long long数组(静态和动态分配)的性能,但msvc除外,它消除了静态数组上的死代码,结果非常可比,因此,我仅显示动态数组的性能。使用time.h的低精度时钟功能,时间标记为毫秒,表示一百万次迭代。

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

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

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

这里有很多有趣的事情:llvm杀死了gcc,这是MSVC的典型斑点优化(它在静态数组上完成了令人印象深刻的死代码消除,然后具有糟糕的填充性能)。尽管我的实现明显更快,但这可能仅是因为它认识到位清除的开销比任何其他设置操作要少得多。

Clang的实现值得关注,因为它的速度明显更快。一些额外的测试表明,它的内存集实际上专用于零-400字节数组的非零内存集要慢得多(〜220ms),与gcc相当。但是,具有800字节数组的非零内存设置不会造成速度差异,这可能就是为什么在这种情况下,其内存集的性能比我的实现差的原因-专业化仅适用于小型数组,截止时间恰好在800字节左右。还要注意,gcc的“ fill”和“ ZERO”没有针对内存集进行优化(查看生成的代码),gcc只是生成具有相同性能特征的代码。

结论:memset并没有像人们想象的那样针对此任务进行真正的优化(否则gcc和msvc以及llvm的memset具有相同的性能)。如果性能很重要,那么memset并不是最终的解决方案,尤其是对于这些笨拙的中型数组,因为它不是专门用于位清除的,而且它的手动优化也没有编译器自己能做的更好。


4
没有代码,没有提及编译器版本和使用的选项的基准?嗯...
Marc Glisse 2015年

我已经有了编译器版本(它们只是隐藏了一些),并添加了使用的适用选项。
本杰明

一元'*'的无效类型参数(具有'size_t {aka unsigned int}')|
Piotr Wasilewicz

如此慷慨地编写自己的优化归零方法-您能在它的工作原理上多说几句吗,为什么它更快呢?该代码几乎是不言自明的。
Motti Shneor '19

1
@MottiShneor看起来比它复杂。AVX寄存器的大小为32bytes。因此,他计算了a一个寄存器中有多少适合的值。之后,他遍历了所有32个字节的块,应该使用指针算术((float *)((a)+x))将其完全覆盖。这两个内部函数(以开头_mm256)仅创建了一个零初始化的32byte寄存器并将其存储到当前指针。这是前3行。其余的仅处理最后32个字节块不应被完全覆盖的所有特殊情况。由于矢量化,速度更快。-希望有帮助。
wychmaster

11

来自memset()

memset(myarray, 0, sizeof(myarray));

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


2
即使在编译时不知道数组的大小,sizeof也将起作用。(当然,仅当它为数组时)
asaelr 2012年

2
@asaelr:在C ++中,sizeof始终在编译时进行评估(并且不能与VLA一起使用)。在C99中,对于VLA,它可以是运行时表达式。
Ben Voigt

@BenVoigt好吧,问题是关于cc++。我评论了Alex的回答,说“如果在编译时知道myarray的大小,则可以使用sizeof(myarray)”。
asaelr

2
@asaelr:在C ++中,他是完全正确的。您的评论未提及C99或VLA,因此我想澄清一下。
Ben Voigt

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)

这将为您提供类似于C ++的功能,使您可以将任何类型的对象数组“重置为零”,而不必诉诸于hack等memset。基本上,这是C ++函数模板的C类似物,除了必须显式指定type参数。

最重要的是,您可以为未衰减的数组构建一个“模板”

#define ARRAY_SIZE(a) (sizeof (a) / sizeof *(a))
#define ZERO_ANY_A(T, a) ZERO_ANY(T, (a), ARRAY_SIZE(a))

在您的示例中,它将作为

int a[100];

ZERO_ANY(int, a, 100);
// or
ZERO_ANY_A(int, a);

还值得注意的是,专门针对标量类型的对象可以实现与类型无关的宏

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

#define ZERO_A(a) ZERO((a), ARRAY_SIZE(a))

把上面的例子变成

 int a[100];

 ZERO(a, 100);
 // or
 ZERO_A(a);

1
我会在;之后省略while(0),所以可以打电话给ZERO(a,n);+1个好答案
0x90 2013年

@ 0x90:是的,您绝对正确。整个do{}while(0)习语都不需要;在宏定义中定义。固定。
AnT

3

对于静态声明,我认为您可以使用:

T myarray[100] = {0};

对于动态声明,我建议采用相同的方式: memset


2
问题是:“不仅用于初始化”。
Ben Voigt

2

zero(myarray); 是C ++中所需的全部。

只需将其添加到标题中:

template<typename T, size_t SIZE> inline void zero(T(&arr)[SIZE]){
    memset(arr, 0, SIZE*sizeof(T));
}

1
这是不正确的,它将清除SIZE个字节。'memset(arr,0,SIZE * sizeof(T));' 是正确的。
Kornel Kisielewicz

@KornelKisielewicz D'哦!我希望在过去的1.5年中没有人复制粘贴此功能:(
纳文

1
希望不是,我评论是因为Google将我带到了这里:)
Kornel Kisielewicz

1
请注意,此参数zero对于例如参数是多维数组T=char[10]时的情况也是正确的。arrchar arr[5][10]
mandrake

1
是的,我使用gcc 4.7.3测试了许多情况。我发现这对于此答案将是一个很好的提示,否则您将需要针对每个数组维数具有模板专门化。其他答案也不能一概而论,例如ARRAY_SIZE宏,如果在多维数组上使用时给出错误的大小,则更好的名称可能是ARRAY_DIM<n>_SIZE
mandrake

1

这是我使用的功能:

template<typename T>
static void setValue(T arr[], size_t length, const T& val)
{
    std::fill(arr, arr + length, val);
}

template<typename T, size_t N>
static void setValue(T (&arr)[N], const T& val)
{
    std::fill(arr, arr + N, val);
}

您可以这样称呼它:

//fixed arrays
int a[10];
setValue(a, 0);

//dynamic arrays
int *d = new int[length];
setValue(d, length, 0);

上面是比使用memset更多的C ++ 11方法。如果使用动态数组并指定大小,也会出现编译时错误。


最初的问题在C上,而不是C ++上,因此std :: fill不是正确的答案
Motti Shneor
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.