【问题标题】:Reset C int array to zero : the fastest way?将 C int 数组重置为零:最快的方法?
【发布时间】:2012-02-27 02:50:22
【问题描述】:

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

对于像 T *myarray = new T[100] 这样的动态数组也有同样的问题。

【问题讨论】:

  • @BoPersson:好吧,new C++...
  • @Matteo - 嗯,是的。对答案影响不大(直到现在:-)。
  • @BoPersson:当涉及到 C++ 时,我只谈论 memset 感觉很糟糕...... :)
  • 在现代编译器上,您无法击败简单的for 循环。但是,令人惊讶的是,你可以通过尝试变得聪明而做得更糟。
  • 使用结构并在其中粘贴一个数组。创建一个全为零的实例。用它来清零你创建的其他人。它运作良好。没有包含,没有功能,非常快。

标签: c++ c arrays memset


【解决方案1】:

memset(来自<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(来自 <algorithm>):

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

其中可能会自动优化为memset;我很确定它对于ints 的运行速度与memset 一样快,而如果优化器不够聪明,它对于较小类型的性能可能会稍差一些。不过,如有疑问,请提供个人资料。

【讨论】:

  • 从 1999 ISO C 标准开始,实际上并不能保证 memset 会将整数设置为 0;没有明确声明全位为零是0 的表示。技术勘误增加了这样的保证,它包含在 2011 ISO C 标准中。我相信对于所有现有 C 和 C++ 实现中的所有整数类型,全位零 的有效表示 0,这就是委员会能够添加该要求的原因。 (浮点或指针类型没有类似的保证。)
  • 添加到@KeithThompson 的评论:此保证已在 TC2 (2004) 中以纯文本形式添加到 6.2.6.2/5 中;但是,如果没有填充位,则 6.2.6.2/1 和 /2 已经保证所有位为零是0。 (对于填充位,存在所有位为零可能是陷阱表示的可能性)。但无论如何,TC 应该承认并替换有缺陷的文本,因此从 2004 年起,我们应该像 C99 始终包含该文本一样行事。
  • 在C语言中,如果你正确分配了动态数组,那么这两个memset之间就没有区别了。正确的动态分配是int (*myarray)[N] = malloc(sizeof(*myarray));
  • @Lundin:当然——如果你在编译时知道N 有多大,但在绝大多数情况下,如果你使用malloc,你只知道在运行时。
  • @MatteoItalia 我们从 1999 年就开始使用 VLA。
【解决方案2】:

这个问题虽然很老,但需要一些基准,因为它要求的不是最惯用的方式,也不是可以用最少的行数编写的方式,而是最快的方式。在没有实际测试的情况下回答这个问题是愚蠢的。所以我比较了四种解决方案,memset 与 std::fill 与 AnT 答案的零与我使用 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 的低精度时钟功能,100 万次迭代的时间标记为 ms。

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 的实现值得更多关注,因为它明显更快。一些额外的测试表明,它的 memset 实际上专门用于零 - 400 字节数组的非零 memset 慢得多(~220ms)并且与 gcc 相当。但是,具有 800 字节数组的非零 memsetting 没有速度差异,这可能就是为什么在这种情况下,他们的 memset 的性能比我的实现差 - 专门化仅适用于小型数组,并且截止值正好在 800 字节左右。另请注意,gcc 'fill' 和 'ZERO' 并未针对 memset 进行优化(查看生成的代码),gcc 只是生成具有相同性能特征的代码。

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

【讨论】:

  • 没有代码且没有提及编译器版本和使用的选项的基准测试?嗯……
  • 我已经有了编译器版本(它们只是有点隐藏),只是添加了使用的适用选项。
  • 一元'*'的无效类型参数(有'size_t {aka unsigned int}')|
  • 如此慷慨地编写了您自己的优化归零方法 - 您能否简单介绍一下它是如何工作的,以及为什么它更快?代码几乎是不言自明的。
  • @MottiShneor 看起来比实际复杂。一个 AVX 寄存器的大小为 32 字节。所以他计算了有多少a 的值适合一个寄存器。之后,他遍历所有 32 字节块,应该使用指针算法 ((float *)((a)+x)) 完全覆盖这些块。这两个内在函数(以_mm256 开头)只是创建一个零初始化的 32 字节寄存器并将其存储到当前指针。这是前 3 行。其余的只是处理不应完全覆盖最后 32 字节块的所有特殊情况。由于矢量化,它更快。 - 我希望这会有所帮助。
【解决方案3】:

来自memset()

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

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

【讨论】:

  • sizeof 将起作用,即使在编译时数组的大小是未知的。 (当然,只有当它是数组时)
  • @asaelr:在 C++ 中,sizeof 总是在编译时评估(并且不能与 VLA 一起使用)。在 C99 中,对于 VLA,它可以是运行时表达式。
  • @BenVoigt 好吧,问题是关于cc++。我评论了 Alex 的回答,上面写着“如果 myarray 的大小在编译时已知,您可以使用 sizeof(myarray)”。
  • @asaelr:在 C++ 中,他是完全正确的。你的评论没有提到 C99 或 VLA,所以我想澄清一下。
【解决方案4】:

您可以使用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++ 的功能,让您可以“重置为零”任何类型的对象数组,而无需求助于像 memset 这样的黑客。基本上,这是 C++ 函数模板的 C 模拟,只是您必须明确指定类型参数。

除此之外,您还可以为非衰减数组构建“模板”

#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);

【讨论】:

  • 我会在while(0) 之后省略;,所以可以致电ZERO(a,n);,+1 好答案
  • @0x90:是的,你完全正确。 do{}while(0) 成语的全部要点在宏定义中不需要 ;。固定。
【解决方案5】:

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

T myarray[100] = {0};

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

【讨论】:

  • 问题说:“不仅仅是为了初始化”。
【解决方案6】:

zero(myarray); 是您在 C++ 中所需要的。

只需将其添加到标题中:

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

【讨论】:

  • 这是不正确的,它会清除 SIZE 字节。 'memset(arr, 0, SIZE*sizeof(T));'是正确的。
  • @KornelKisielewicz 哦!我希望在过去的 1.5 年里没有人复制粘贴这个功能 :(
  • 希望不是,我评论是因为 google 把我带到了这里 :)
  • 请注意,此函数zero 也适用于例如T=char[10] 可能是 arr 参数是多维数组时的情况,例如char arr[5][10].
  • 是的,我用 gcc 4.7.3 测试了一些案例。我发现这个答案最好注意这一点,因为否则您需要为每个数组维度计数提供模板专业化。其他答案也不能一概而论,例如ARRAY_SIZE 宏,如果在多维数组上使用它会给出错误的大小,更好的名称可能是ARRAY_DIM&lt;n&gt;_SIZE
【解决方案7】:

这是我使用的函数:

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 不是正确答案
猜你喜欢
  • 2017-01-27
  • 1970-01-01
  • 1970-01-01
  • 2023-03-28
  • 1970-01-01
  • 2012-02-09
  • 1970-01-01
  • 2023-04-04
  • 2012-08-10
相关资源
最近更新 更多