为啥数组声明的顺序对性能影响如此之大?
Posted
技术标签:
【中文标题】为啥数组声明的顺序对性能影响如此之大?【英文标题】:Why does order of array declaration affect performance so much?为什么数组声明的顺序对性能影响如此之大? 【发布时间】:2012-08-10 13:16:17 【问题描述】:首先,在使用 Accelerate 框架调整频率分析函数时,每次迭代的绝对系统时间始终为 225 毫秒。然后昨晚我改变了声明两个数组的顺序,突然它下降到 202ms。仅通过更改申报顺序来增加 10% 似乎很疯狂。有人可以向我解释为什么编译器(设置为优化)还没有找到这个解决方案吗?
附加信息:在循环之前,对循环中使用的数组进行了一些设置,包括将它们从整数转换为浮点数组(用于加速),然后取时间数组的 sin 和 cos(16 行长)。所有的浮点数组(8 个数组 x 1000 个元素)首先在函数中声明(在对参数进行完整性检查之后)。它们总是被声明为相同的大小(由一个常数),否则性能会因占用空间的小幅收缩而受到影响。我测试了将它们设为全局,但我认为编译器已经发现了这一点,因为没有性能变化。循环长 25 行。
---添加---
是的,“-Os”是标志。 (无论如何,Xcode 中的默认值:最快、最小)
(以下来自记忆 - 不要尝试编译它,因为我没有输入步幅(即 1)等内容。但是,所有的 Accelerate 调用都在那里)
传递的参数:inttimearray、intamparray、length、scale1、scale2、amp
float trigarray1[maxsize];
float trigarray2[maxsize];
float trigarray3[maxsize];
float trigarray4[maxsize];
float trigarray5[maxsize];
float temparray[maxsize];
float amparray[maxsize]; //these two make the most change
float timearray[maxsize]; //these two make the most change
vDSP_vfltu32(inttimearray,timearray,length); //convert to float array
vDSP_vflt16(intamparray,amparray,length); //convert to float array
vDSP_vsmul(timearray,scale1,temparray,length); //scale time and store in temp
vvcosf(temparray,trigarray3,length); //cos of temparray
vvsinf(temparray,trigarray4,length); //sin of temparray
vDSP_vneg(trigarray4,trigarray5,length); //negative of trigarray4
vDSP_vsmul(timearray,scale2,temparray,length); //scale time and store in temp
vvcosf(temparray,trigarray1,length); //cos of temparray
vvsinf(temprray,trigarray2,length); //sin of temparray
float ysum;
vDSP_sve(amparray,ysum,length); //sum of amparray
float csum, ssum, ccsum, sssum, cssum, ycsum, yssum;
for (i = 0; i<max; i++)
vDSP_sve(trigarray1,csum,length); //sum of trigarray1
vDSP_sve(trigarray2,ssum,length); //sum of trigarray2
vDSP_svesq(trigarray1,ccsum,length); //sum of trigarray1^2
vDSP_svesq(trigarray2,sssum,length); //sum of trigarray2^2
vDSP_vmul(trigarray1,trigarray2,temparray,length); //temp = trig1*trig2
vDSP_sve(temparray,cssum,length); //sum of temp array
// 2 more sets of the above 2 lines, for the 2 remaining sums
amp[i] = (arithmetic of sums);
//trig identity to increase the sin/cos by a delta frequency
//vmma is a*b+c*d=result
vDSP_vmma (trigarray1,trigarray3,trigarray2,trigarray4,temparray,length);
vDSP_vmma (trigarray2,trigarray3,trigarray1,trigarray5,trigarray2,length);
memcpy(trigarray1,temparray,length*sizeof(float));
---当前解决方案---
我做了如下改动:
数组都被声明为对齐,并被归零(我将在接下来解释)并且 maxsize 现在是 16 的倍数
__attribute__ ((align (16))) float timearray[maxsize] = 0;
我已将所有数组归零,因为现在,当长度小于 maxsize 时,我将长度四舍五入到最接近 16 的倍数,以便所有循环函数在可被 16 整除的宽度上运行,不影响总和。
好处是:
性能略有提升 无论数组声明的顺序如何,速度几乎都是恒定的(现在在需要它们之前就完成了,而不是全部放在一个大块中) 对于任何 16 宽长度(即 241 到 256,或 225 到 240...),速度也几乎恒定,而在此之前,如果长度从 256 变为 255,则该函数将占用 3+%性能受到影响。在未来(可能使用此代码,因为分析要求仍在不断变化),我意识到我需要更多地考虑堆栈的使用,以及向量的对齐/块。不幸的是,对于这段代码,我不能将这些数组设为静态或全局,因为该函数一次可以被多个对象调用。
【问题讨论】:
你能通过交换数组声明顺序回到 225ms 吗? 是的,这两个,始终如一。而且我发现如果我切换其他阵列,我最终会得到更差的性能 290+ms 或其他配置 210ms。现在它们都在一行中声明(每行一个,以便更快地测试订单)。但是,我确实尝试在需要之前将每个声明移动到正确的位置,并获得了 290 毫秒。 可以给我们看看代码吗?至少对于声明来说,如果它们都在一起的话。 会不会是一些缓存的东西?如果数组按一个顺序排列,也许您更有可能在一个操作中提取下一个操作所需的数据。 【参考方案1】:我首先怀疑的是对齐。您可能想尝试:
__attribute__ ((align (16))) float ...[maxsize];
或者确保maxsize
是 16 的倍数。如果在一种配置中对齐而在另一种配置中不对齐,这肯定会导致 10% 的命中率。向量运算可能对此非常敏感。
您可能遇到的下一个主要问题是大量堆栈(假设 maxsize
相当大)。 ARM 处理小于 4k 的数字比处理大于 4k 的数字更有效(因为它只能处理 12 位立即数)。因此,根据编译器对其进行优化的方式,将 amparray 向下推入堆栈可能会导致更复杂的数学运算来访问它。
当小事导致性能发生大变化时,我总是建议调出程序集(产品>生成输出>程序集)并查看编译器输出的变化。我还强烈推荐 Whirlwind Tour of ARM Assembly 让您开始了解您正在查看的内容。 (确保将输出设置为“用于存档”,以便看到优化的结果。)
你还应该做更多的事情:
尝试将此例程重写为简单的 C,而不是使用 Accelerate。是的,我知道 Accelerate 总是更快,但事实并非如此。所有这些函数调用都非常昂贵,编译器通常可以更好地向量化简单的乘法和加法,而 Accelerate 可以根据我的经验。如果您的步幅为 1,您的向量不是很大,并且您使用的是 1-2 核心设备(如 iPad),则尤其如此。当您拥有处理跨步的代码时(如果您不需要跨步),它比您手动编写的代码更复杂(更慢)。根据我的经验,Accelerate 似乎确实非常擅长斜坡和超越(例如大表的余弦),但在简单的向量和矩阵数学方面几乎没有那么擅长。
如果此代码真的对您很重要,我发现手写程序集绝对可以超过编译器。我什至不擅长 ARM 汇编程序,而且我已经能够在简单的矩阵数学上击败编译器 2 倍(并且编译器压垮了 Accelerate)。我在这里特别谈论你的循环,它似乎只是在做加法和乘法。手写程序集当然很痛苦,然后你必须为汇编程序维护一个 C 版本,但当它真的很重要时,它真的很快。
【讨论】:
最近我改变了我的分析块,现在意识到我可以大大减少 maxsize - 这可能会使这成为一个无用的练习。最初所有代码都是直接 C 语言,并且比我用 Accelerate 替换它时慢 10 倍(vdsp_vmma 和 vvsinf 比 C 快得多) - 同样是当块更大,迭代数组长度和索引是可变的(现在都不是真的)。我现在的时间是可以接受的,我不确定 ASM 是否值得花时间。但是,我对对齐方式感到好奇-我尝试过该属性没有效果。对此有什么想法吗? 感谢有关 vdsp_vmma 的说明。我绝对同意 vvsinf;没有分析 vmma。如果数据已经对齐(这可能是由于意外或优化而发生的),或者如果特定函数不太依赖它,对齐并不总是获胜。有时他们会进行单独的迭代来处理领先的未对齐值,直到他们可以得到对齐的数据。有时它们只是不需要对齐的数据。 谢谢。我将再次对齐,并使用 maxsize。我去 Accelerate 的全部原因是块大小是-巨大的-。而且我只是在迭代上花费了太多时间。既然我已经前往一个较小的街区,那么直接 C 可能会更快而没有开销。但是,我现在不会删除它,因为它使函数更具可读性(同样,并非我的所有代码都显示 - 但缺少的行都是简单的算术)。【参考方案2】:如果没有可运行的代码,可能很难确定存在哪些性能障碍。
我将使用这个答案来提出一些可能性,并对这个问题的其他答案和 cmets 中提出的一些问题发表评论。
首先,对于 7 个每个 4 kB 的数组,您使用的几乎是 L1 缓存的大小。根据堆栈使用的其他数量等,您可能会破坏缓存。这可以解释为什么减少块大小会提高性能:对于较小的块,每次迭代使用的内存更少,并且所有这些都适合缓存,因此在迭代期间很少或没有丢弃。处理这种缓存抖动的另一种方法是条带挖掘:不是在整个长度上执行 sve、svesq、vmul、vmma 和 memcpy,而是在长度的一部分(例如一半)上执行所有这些,然后执行所有这些都放在另一部分,并根据需要重复,直到它们被完全处理。
trigarray5 仅存在以便第二个 vmma 否定 trigarray4。消除 trigarray5 并使用 trigarray4 调用 vmmsb(减去而不是添加)。这也减少了内存使用。
即使使用的数据少于填充缓存,缓存几何有时也会导致抖动。缓存被划分为多个集合,每个内存地址必须映射到一个特定的集合。例如,一个 32,768 字节的高速缓存可能有 1024 个“行”,每行 32 个字节,但它可以组织成 256 组,每组四行。任何一个内存地址都映射到一组,并且它必须使用该组中的四行之一。如果您有五个数组以相同的地址开始,以该几何为模(或基本上重叠),那么它们将争夺每组中的四行,并在它们进行时相互排斥。当数组在内存中连续分配时,可以避免这种情况,就像编译器通常在一个接一个地简单地声明数组时所做的那样,但可能会出现复杂情况。没有可运行的代码,很难确定。
将数组对齐为 16 字节的倍数很好,可能会有所帮助。在某些情况下,它有很大帮助。如果可能,许多 vDSP 例程会处理一些初始元素以达到良好对齐的边界,然后使用快速 SIMD 代码直到接近数组末尾,此时可能需要单独处理另外一些元素。然而,这并不总是可能的,因为当一个对多个向量进行操作的例程被传递给具有不同对齐方式的向量时。 (对齐一个指针的处理元素会使其他指针未对齐。)除了添加 align 属性,对齐数组的另一种方法是使用标准内存分配例程分配它们,例如 malloc。在 Mac OS X 和 ios 上,malloc 返回 16 字节对齐的地址。
堆栈大小和 ARM 具有有限立即数的事实可能不是问题,向量地址的计算应该是代码中计算的一个微不足道的部分。 (此外,ARM 有一些有趣的灵活立即数,而不仅仅是 12 位整数。)
实际函数调用和返回本身的成本可能微不足道。 Apple 提供的编译器并没有“比 Accelerate 更好地矢量化简单的乘法和加法”,而且函数调用也不是“非常昂贵”。
你忽略了大步。如果它们不是一个,您可能会通过重写代码获得很多好处,以便在调用 vDSP 例程时数据具有单位跨度。
分支预测在这里可能不是问题。
可运行代码将极大地帮助诊断您的性能问题。
【讨论】:
为了以后参考别人,我没有使用vmmsb,因为它比创建负数组和使用vmma慢得多。我没有想过一次做块——这对于巨大的向量来说是个好主意。从您和其他人的回复中我可以看出,未来我需要比以前更加关注内存使用情况。 vmmsb 和 vmma 在当前 iOS 版本中的性能应该相同。 (源代码看起来是相同的,除了加减法的变化。)在 Mac 上,vmma 已优化,而 vmmsb 则没有。这是一个疏忽;请向bugreport.apple.com 提交请求以优化vmmsb。如果您看到 ARM 性能下降,这可能是一种错觉,因为讨论了其他影响。如果可以重现,请提交错误报告,并说明具体的软件版本,特别是目标操作系统版本和设备型号。 你是对的。我正在测试一个裸 Mac 应用程序中的功能。但是,我只是在模拟器和手机上的一个裸 iOS 应用程序中对其进行了测试。在模拟器上,使用 vmmsb 时,我得到了大约 40% 的减速(在 Mac 应用程序上并没有这么糟糕),而在 iPhone 上,性能完全相同。谢谢,你救了我一些记忆。【参考方案3】:可能与分支预测以及数组中的元素有关。
请参阅此帖子以获取令人敬畏的参考。您的帖子可能与此帖子类似,通过以一种顺序声明您的数组,数据显示为“排序”,但按另一种顺序,则不是。
Why is it faster to process a sorted array than an unsorted array?
【讨论】:
该代码中的任何地方都没有 IF 语句。据我了解,没有条件,没有什么可以分支的。顺便说一句,我已经测试了为 Accelerate 数组添加 __property___((aligned)) 但似乎编译器已经对齐了它们。 你绝对有一个分支。它在 for() 中。我并不是说分支预测是你的问题,但肯定有一个条件被称为max
次。
for
上的分支预测在这种情况下可能是微不足道的,因为它会被 vDSP 例程中的工作淹没。即使for
的分支从分支历史缓存中掉出并且每次都被错误预测,它也不应该对性能产生太大影响。
改变数组的顺序并不会改变数组内项目的排序。
当然。我并没有真正仔细阅读这个问题,只是阅读了我链接的主题,这听起来像是一个可能的原因。在实际查看了 Adam 的代码之后,我发现分支预测与它无关。【参考方案4】:
这里只是猜测。对齐?
那些库应该使用 SIMD 指令,并且这些指令的时间取决于对齐,即使在某些不需要对齐的情况下也是如此。
缓存行对齐也可能起作用,也可能不起作用。
这些数组是在堆栈上分配的,这意味着除了 sizeof(float) 内在保证和第一个对象的体系结构保证之外,您几乎无法控制该数据的对齐方式(第一个对象事实上保证了 64 位对齐)如果您在 64 位模式下编译,则为局部变量)。
您可以尝试通过打印/记录地址来验证数据对齐方式。并通过定义一个结构来保存数据并使用 malloc 为其获取内存(获得比您需要的更多的内存,以便您可以将结构放置在内存块中的不同偏移量处,特别是如果您想玩缓存线对齐)。
【讨论】:
【参考方案5】:首先:不幸的是,这种对数据放置的敏感性很常见。我们中的一些人编写了尝试多种不同布局的代码
导致此类性能损失的常见罪魁祸首是:
分支错误预测
缓存效果
容量未命中(只是数据过多,例如 1MB 的数据无法放入 32KB 的缓存中)
缓存冲突(例如,在 4 路关联 32KB 缓存中,超过 4 个相同的模 8K 地址)
DRAM 效果
DRAM 页面缺失我无法解析您所说的内容:MAXSIZE 是什么?你说 7*4KB ......但是你有 8 个数组,所以我怀疑你是说 MAXSIZE=1024。你是说MAXSIZE是7*1024? (* 4B / 浮动?)
无论如何:如果每个单独阵列的 MAXSIZE 大约为 28KB,那么对于许多系统来说,您的缓存大小已经接近。在这种情况下,我会怀疑 DRAM 页面效应 - 我会怀疑性能良好的安排会将访问次数最多的数组放在单独的 DRAM 页面中。
你没有说哪个表现更好,但我猜:
float amparray[maxsize]; //these two make the most change
float timearray[maxsize]; //these two make the most change
观察您的代码,timearray 似乎是最常访问的。如果 timearray 秒的性能更好,并且我对 MAXSIZE 的猜测是正确的,那么我敢打赌这是 DRAM 页面效应。
快速解释:DRAM 具有页和存储体的概念。不要与操作系统页面混淆。每个 DRAM 芯片,因此每个 DIMM,都有 4 或 8 个内部存储体。每个银行可以有一个打开的页面。如果您从同一页面、同一银行访问数据,则速度最快。如果您从不同银行中已打开的页面访问数据,速度快,但比同一银行的同一页面慢如果您需要同一银行中的不同页面,真的很慢。如果你有一个写回缓存,那么写回几乎是随机发生的,所以你会得到非常糟糕的页面行为。
但是,如果我猜错了 MAXSIZE,那么可能是缓存效应。
RED FLAG:你说“我没有加入大步之类的东西”。 Strides 因使数据在缓存中表现不佳而臭名昭著。缓存通常是设置关联的,这意味着它们具有我所说的“共振” - 与缓存的共振模数相同的地址将映射到同一组。如果你有比联想更多的东西,你会痛的。
将共振计算为缓存大小除以关联性。例如。如果你有一个 32K 的 4 路关联缓存,你的共振是 8K。
无论如何...如果您只是快速访问内容,那么数组放置可能很重要。例如。假设您的步幅为 16。即访问元素 0、16、32、48 等。如果 MAXSIZE 为 7*1024,正如我上面猜到的,那么元素
float trigarray1[maxsize];
float trigarray2[maxsize];
float trigarray3[maxsize];
float trigarray4[maxsize];
float trigarray5[maxsize];
float temparray[maxsize];
float amparray[maxsize]; //these two make the most change
float timearray[maxsize]; //these two make the most change
那么以下数组将发生冲突 - 它们的跨步访问模式将映射到相同的集合:
trigarray1, trigarray5
trigarray2, temparray
trigarray3, amparray
trigarray4, timearray,
如果你把 amparray 和 timearray 互换,那么
trigarray3 will conflict with timearray
and
trigarray4 with amparray
trigarray4 和 timarray 似乎是最常用的,所以我猜,如果你的步幅像 0、16、32、348,或者实际上任何以 0 开头的步幅,那么这两个数组冲突就是你的问题。
但是,您可能有不同的步幅模式:0、16、32、48 ... 在一个数组中,而 1,17,33,... 在另一个数组中。那么不同的数组对就会发生冲突。
--
我没有足够的信息来诊断您的问题。
如果您可以使用性能良好的工具,您或许可以自己完成。
例如在英特尔处理器上,您可以记录我所说的缓存未命中配置文件,记录理想的物理内存地址,计算它们在缓存中映射到的集合,并生成直方图。如果您看到尖峰,那可能是个问题。同样,您可以生成 DRAM 页面未命中或存储库未命中配置文件。我只提到英特尔是因为我设计了一些硬件来实现这种性能测量。同样的事情可能应该在 ARM 上可用(如果没有,也许我可以得到丰富的销售工具来做到这一点......:-))。
如果是这些问题,你该如何解决?
好吧,通过尝试不同的展示位置,正如您在上面解释的那样。这可以帮助解决跨步(缓存集冲突)和 DRAM 页面问题。
如果步幅有问题,您可以尝试使数组大小稍微不同 - MAXSIZE + 4、MAXSIZE 8 等。这可以有效地抵消步幅。 (在超级计算机代码中看到大小为 255 或 257 的数组很常见,这与偏移跨步访问模式的原因相同,以免发生冲突。)
【讨论】:
以上是关于为啥数组声明的顺序对性能影响如此之大?的主要内容,如果未能解决你的问题,请参考以下文章