【问题标题】:How to fast initialize with 1 really big array如何使用 1 个非常大的数组快速初始化
【发布时间】:2013-05-05 14:21:38
【问题描述】:

我有大阵:

int* arr = new int[BIGNUMBER];

如何快速填写 1 个号码。通常我会这样做

for(int i = 0; i < BIGNUMBER; i++)
    arr[i] = 1

但我认为这需要很长时间。

我可以使用memcpy 或类似的吗?

【问题讨论】:

  • 尽量避免假设运行需要多长时间。
  • 您真的尝试过这样做吗?需要多长时间?
  • BIGNUMBER 通常是什么?
  • 如果这是您的瓶颈,那么您的代码结构错误。我猜你填充数组是有原因的,所以你必须有另一个(可能更慢)循环。无论如何memcpy 可能会更慢,因为它需要进行内存查找而不是使用寄存器/常量。
  • 仅供参考:我尝试了基于 memcpy 的算法,但在速度上没有得到任何有意义的差异。

标签: c++ arrays performance


【解决方案1】:

您可以尝试使用标准函数std::uninitialized_fill_n

#include <memory>

// ...

std::uninitialized_fill_n(arr, BIGNUMBER, 1);

在任何情况下,在性能方面,规则是始终进行测量以支持您的假设 - 特别是如果您因为所谓的性能改进而放弃清晰、简单的设计而采用更复杂的设计.

编辑:

请注意 - as Benjamin Lindley mentioned in the comments - 对于普通类型 std::uninitialized_fill_n 并没有比更明显的 std::fill_n 带来任何优势。非平凡类型的优势将存在,因为std::uninitialized_fill 将允许您分配内存区域,然后就地构造对象。

但是,不应陷入为未初始化的内存区域调用std::uninitialized_fill_n 的陷阱。。例如,以下会给出未定义的行为:

my_object* v = new my_object[BIGNUMBER];
std::uninitialized_fill_n(my_object, BIGNUMBER, my_object(42)); // UB!

【讨论】:

  • @RobertKilar:之前尝试使用std::uninitialized_fill_n() 函数,并检查它是否适合您。很可能该函数最终会为琐碎的类型调用 memcpy(),并且所有内容都会被内联。
  • 在这种情况下,uninitialized_fill_n 比更明显的 fill_n 提供什么?
  • @BenjaminLindley:确实,对于普通类型没有任何意义 - 除了名称更清楚地表明将填充未初始化的内存区域。
  • 我用BIGNUMBER= 1&lt;&lt;31在Linux上用gcc-4.7 -O3测试了OP解决方案和std::uninitialized_fill,性能没有显着差异。
  • 虽然对于非平凡的类型,这将是未定义的行为,因为内存已经使用有效对象进行了初始化。您必须使用fill_n。这是我的(小)反对意见,它对所有类型都更加一致。但我也理解你的推理。
【解决方案2】:

动态数组的替代方案是std::vector&lt;int&gt;,其构造函数接受每个元素的初始值:

std::vector<int> v(BIGNUMBER, 1); // 'BIGNUMBER' elements, all with value 1.

如前所述,需要衡量性能。这种方法提供了额外的好处,即内存将被自动释放。

【讨论】:

  • 但它不能比 ops 解决方案更快,因为它在内部必须执行相同的任务:第一个:分配内存,第二个:用默认值填充它
【解决方案3】:

Andy Prowl 的 std::uninitialized_fill_n() 解决方案的一些可能替代方案,仅供后代使用:

  • 如果你很幸运,并且你的值由所有相同的字节组成,memset 会成功。
  • 某些实现提供 16 位版本 memsetw,但并非无处不在。
  • GCC 有一个Designated Initializers 的扩展,可以填充范围。
  • 我曾使用过一些 ARM 系统,它们的库具有加速 CPU 和 DMA 字填充变体的库,并在汇编中手工编码 - 如果您不是,您可能会查看您的平台是否提供这些功能非常担心可移植性。
  • 根据您的处理器,即使查看 SIMD 内部函数的循环也可能会提供提升;一些 SIMD 单元具有加载/存储管道,这些管道针对像这样移动数据进行了优化。另一方面,您可能会因在寄存器类型之间移动而受到严厉处罚。

最后但同样重要的是,回应一些评论者:您应该测试并查看。编译器往往非常擅长识别和优化这样的模式——您可能只是在使用简单循环或uninitialized_fill_n以外的任何东西来权衡可移植性或可读性。

您可能对之前的问题感兴趣:

【讨论】:

  • memset 必须处理范围开头和结尾的非对齐写入。 std::fill_n&lt;int&gt; 没有。 memset 的智能实现实际上可能最多写入 3 个字节,然后调用 std::fill_n(aligned_ptr, value * 0x01010101U, n/4),然后以最后未对齐的字节结束。
  • @MSalters:绝对正确,但我在这里有过一些奇怪的经历。例如,在我使用过的一个平台上,每字节副本(烦人地不是 memcpy)和 memset 都有大量手动优化的代码:不同大小的分支目标、缓存线感知等。一次被略微优化,利用所有可用的 ARM 寄存器,因此它一次读取大约 32 个字节,一次写入 32 个字节,但它不支持缓存线。不过,它的 icache 占用空间要低得多。
  • @MSalters:绝大多数时候,“8 位”副本要快得多,尽管/因为所有的分支和准备工作都是为了找到缓存行的开头,处理短副本等...每隔一段时间,由于 icache 污染或特别小的复制量,复制一词会胜出。
  • 我不确定这是否会有所作为,并且肯定会涉足可维护性/可移植性与速度线,但我确实想指出可能存在的替代方案。 =)
  • ARM,在所有 CPU 中,有更快的字节复制?!字节写入基本上是对单词的读取-修改-写入操作的设计?我很惊讶。
【解决方案4】:

在启用优化的 Linux/x86 gcc 下,您的代码将编译为以下内容:

rax = arr
rdi = BIGNUMBER

400690: c7 04 90 01 00 00 00    movl   $0x1,(%rax,%rdx,4)

立即将int(1) 移动到 rax + rdx

400697: 48 83 c2 01             add    $0x1,%rdx

递增寄存器rdx

40069b: 48 39 fa                cmp    %rdi,%rdx

Cmp rdi 到 rdx

40069e: 75 f0                   jne    400690 <main+0xa0>

如果已达到 BIGNUMBER,则跳回开始。

在我的机器上每 GB 大约需要 1 秒,但我敢打赌,其中大部分是在物理内存中分页以支持未初始化的分配。

【讨论】:

  • 开启更多优化,事情就会变得有趣;-)
  • @MarcGlisse:那就是-O3,你建议开启哪些额外的优化?
  • -march=native,也许试试更新的编译器?
  • @MarcGlisse:同样的结果,我使用的是 gcc 4.7。
【解决方案5】:

只需将循环展开 8 或 16 次。 memcpy 之类的函数速度很快,但它们确实是为了方便,而不是比你可能编写的任何东西都快:

for (i = 0; i < BIGNUMBER-8; i += 8){
  a[i+0] = 1; // this gets rid of the test against BIGNUMBER, and the increment, on 7 out of 8 items.
  a[i+1] = 1; // the compiler should be able to see that a[i] is being calculated repeatedly here
  ...
  a[i+7] = 1;
}
for (; i < BIGNUMBER; i++) a[i] = 1;

编译器可能会为您展开循环,但为什么要冒险呢?

【讨论】:

  • @Marc:你怎么理解的?
  • 编译器带有一个称为矢量化器的优化。它可以识别对数组的所有项执行相同操作的循环,生成序言/结语来处理对齐,并为主循环生成向量指令。如果 a 没有(已知)很好地对齐,那么您的展开只会使矢量化变得非常困难(或者它可能会使用较慢的未对齐写入来矢量化)。编译器还可以根据目标 CPU 展开适量的循环。另外,这对人类来说可读性较差;-)
  • 我会说先看看组件;在你按摩你的代码之前,希望它会被矢量化。上次我尝试时,只是对自己进行矢量化更容易,GCC 拒绝在那个特定时间播放。
  • @Marc:也许有些程序员看不懂。我希望不是 ;-) 但你的观点很好。总是值得看看编译器生成的汇编语言,并将其调侃为生成好东西。谢谢。
【解决方案6】:

使用 memset 或 memcpy

memset(arr, 0, BIGNUMER); 

【讨论】:

  • memset 不适用于非零数字,因为它适用于字节级别。 memcpy 可能会比手动循环慢,因为它需要继续引用其他内存以及写入
  • @Dave:你的意思是非字节值? memset 绝对适用于任意无符号字符。
  • @Dave memcpy 也会变慢,因为你必须从某个地方复制,这意味着你必须用 1 填充一个数组,然后在上面使用 memcpy。仅仅用 1 填充你的数组似乎很愚蠢。
  • @user268396 不,我的意思是memset(arr,1,BIGNUMBER) 将用 16843009 而不是 1 填充数组。
  • @Dave:在读回时这是一个微妙的类型问题,因为int 由比unsigned char 更多的字节组成。然而事实上,您的评论似乎暗示memset(arr, 0xA, BIGNUMBER) 将无法用AAAA 填充数组(假设为32 位整数)。哪个绝对有效。 ;)
【解决方案7】:

尝试使用 memset 吗?

memset(arr, 1, BIGNUMBER);

http://www.cplusplus.com/reference/cstring/memset/

【讨论】:

  • memset 不适用于非零数字,因为它适用于字节级别。
  • 那么你一定在char数组上使用过它。在 int 数组(如问题中)上,它会产生错误的结果。例如memset(arr,1,...) 会将数字设置为 16843009(假设为 32 位 int)。对于浮点数组,结果将更难以预测。
【解决方案8】:

memset(arr, 1, sizeof(int) * BIGNUMBER);

【讨论】:

  • 这不一样 memset 将每个 Byte 设置为给定值(被视为 uint8_t),因此向量中的每个 int 在之后的值都是 16.843.009那个手术
猜你喜欢
  • 1970-01-01
  • 2012-05-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-20
  • 1970-01-01
  • 2016-05-27
  • 2023-02-11
相关资源
最近更新 更多