为什么数组声明的顺序如此影响性能呢?

时间:2012-08-10 13:16:17

标签: arrays performance accelerate-framework vdsp

首先,在使用Accelerate框架调整频率分析功能时,绝对系统时间一直是每次迭代225ms。然后昨晚我改变了两个阵列声明的顺序,然后突然下降到202ms。仅通过更改申报单增加10%似乎很疯狂。有人可以向我解释为什么编译器(设置为优化)还没有找到这个解决方案吗?

附加信息:在循环之前,循环中使用的数组有一些设置,包括将它们从整数转换为浮点数(用于加速),然后获取时间数组的sin和cos(16行长)。所有浮点数组(8个数组x 1000个元素)都在函数中首先声明(在参数的完整性检查之后)。它们总是被声明为相同的大小(通过常数),因为否则性能会受到足迹收缩的影响。我测试过将它们作为全局变量,但我认为编译器已经确定了这一点,因为没有性能变化。循环长25行。

--- ---附加

是的,“-Os”是旗帜。 (默认情况下在Xcode中:最快,最小)

(下面是来自内存 - 不要尝试编译它,因为我没有输入像stride(这是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 +%表现很好。

在未来(可能使用此代码,因为分析要求仍在不断变化),我意识到我需要更多地考虑堆栈使用,以及向量的对齐/块。不幸的是,对于这段代码,我不能使这些数组成为静态或全局变量,因为这个函数一次可以由多个对象调用。

5 个答案:

答案 0 :(得分:3)

我怀疑的第一件事是对齐。您可能想要试验:

__attribute__ ((align (16))) float ...[maxsize];

或者确保maxsize是16的倍数。如果在一个配置中你是对齐的,那么肯定会导致10%的命中率而在另一个配置中你不会。矢量操作对此非常敏感。

您可能遇到的下一个主要问题是一个巨大的堆栈(假设maxsize相当大)。 ARM可以处理小于4k的数字,而不是处理大于4k的数字(因为它只能处理12位立即数)。因此,根据编译器对其进行优化的方式,将amparray推向堆栈可能会导致更复杂的数学访问。

当小小的东西导致性能发生重大变化时,我总是建议拉起装配(Product&gt; Generate Output&gt; Assembly)并查看编译器输出中的变化。我还强烈建议您Whirlwind Tour of ARM Assembly让您开始了解您正在查看的内容。 (确保将输出设置为“For Archiving”,以便查看优化结果。)

你还应该做更多的事情:

  • 尝试将此例程重写为简单C而不是使用Accelerate。是的,我知道Accelerate总是更快,除非它不是。所有这些函数调用都非常昂贵,并且编译器通常可以更好地矢量化简单的乘法和加法,根据我的经验,Accelerate可以。如果你的步幅是1,你的矢量不是很大,你就像iPad一样在1-2核设备上,这一点尤其如此。当你拥有处理步幅的代码时(如果你不需要步幅),它比你手工编写的代码更复杂(更慢)。根据我的经验,Accelerate似乎非常擅长斜坡和超越(例如大桌子的余弦),但在简单的矢量和矩阵数学方面并不是那么擅长。

  • 如果这段代码真的对您很重要,我发现手写程序集绝对可以超越编译器。我甚至不擅长ARM汇编程序,并且我已经能够在简单矩阵数学上击败编译器2倍(并且编译器压缩了Accelerate)。我特别在谈论你的循环,这似乎只是增加和倍增。手写程序集当然很痛苦,然后你必须为汇编程序维护一个C版本,但是当它真的很重要时它真的很快。

答案 1 :(得分:2)

如果没有可运行的代码,可能很难确定存在哪些性能障碍。

我将使用这个答案来提出一些可能性并对其他答案中提出的一些问题发表评论并对这个问题发表评论。

首先,使用7个每个4 kB的阵列,您使用的是大约L1缓存的大小。根据堆栈使用的其他数量等,您可能会破坏缓存。这可以解释为什么减小块大小可以提高性能:使用更小的块,每次迭代使用更少的内存,并且所有内存都适合缓存,因此在迭代期间很少或没有抛出任何内容。处理这种高速缓存抖动的另一种方法是条带挖掘:不是在整个长度上执行sve,svesq,vmul,vmma和memcpy,而是在长度的一部分上执行所有这些(例如,一半),然后执行所有这些都在另一部分,并在必要时重复,直到它们被完全处理。

trigarray5只存在,因此第二个vmma否定了trigarray4。使用trigarray4消除trigarray5并调用vmmsb(减去而不是添加)。这也减少了内存使用。

即使使用的数据少于填充缓存,缓存几何有时也会导致抖动发生。缓存被分区为集合,每个内存地址必须映射到特定集合。例如,具有32,768字节的高速缓存可以具有每个32字节的1024“行”,但是它可以被组织成256组,每行4行。任何一个内存地址都映射到一个集合,它必须使用该集合中的四行之一。如果你有五个数组以相同的地址开始模数这个几何(或基本上重叠),那么他们将争夺每一组中的四条线,随着它们走向彼此。当数组在内存中连续分配时,可以避免这种情况,因为编译器通常会在数组简单地一个接一个地声明时执行,但可能会出现复杂情况。没有可运行的代码,很难确定。

将数组对齐到16个字节的倍数很好,可能会有所帮助。在某些情况下,它会有很大帮助。在可能的情况下,许多vDSP例程会处理一些初始元素以达到良好对齐的边界,然后使用快速SIMD代码直到接近数组末尾,此时可能需要单独处理另外几个元素。然而,这并不总是可行的,因为当对多个矢量进行操作的例程传递具有不同比对的矢量时。 (处理元素以对齐一个指针会使其他指针不对齐。)除了添加align属性之外,另一种对齐数组的方法是使用标准内存分配例程(例如malloc)来分配它们。在Mac OS X和iOS上,malloc返回16字节对齐的地址。

堆栈大小和ARM限制立即值的事实可能不是问题,矢量地址的计算应该是代码中计算的一个微不足道的部分。 (另外,ARM有一些有趣的灵活立即值,而不仅仅是12位整数。)

实际函数调用和返回本身的成本可能微不足道。 Apple提供的编译器并没有“比Accelerate更好地矢量化简单的乘法和加法”,并且函数调用并不“非常昂贵”。

你省略了步幅。如果它们不是一个,那么通过重写代码可能会获得很多收益,以便在调用vDSP例程时数据具有单位步幅。

分支预测可能不是问题。

可运行代码可以极大地帮助您诊断性能问题。

答案 2 :(得分:0)

可能与分支预测以及阵列中的元素有关。

有关AWESOME的参考信息,请参阅此帖子。你的帖子可能与这篇文章类似,通过在一个顺序中声明你的数组,数据显示为“已排序”,但在另一个顺序中,它不是。

Why is it faster to process a sorted array than an unsorted array?

答案 3 :(得分:0)

这里只是一个猜测。对准?

这些库应该使用SIMD指令,即使在不需要对齐的某些情况下,它们的时序也取决于对齐。

高速缓存行对齐可能也可能不起作用。

这些数组是在堆栈上分配的,这意味着你几乎无法控制数据的对齐超出sizeof(float)内在保证和第一个对象的架构保证(第一个对象事实上保证了64位对齐)如果以64位模式编译,则为局部变量。)

您可以尝试通过打印/记录地址来验证数据对齐方式。通过定义一个保存数据的结构并使用malloc为它获取内存来解决对齐的时序效应(获得比你需要的内存更多的内存,这样你就可以将结构置于内存块的不同偏移处,特别是如果你想要使用缓存行对齐方式)。

答案 4 :(得分:0)

首先:遗憾的是,这种对数据放置的敏感性很常见。我们中的一些人编写了尝试多种不同布局的代码

这种性能损失的常见罪魁祸首是:

  • 分支误预测

  • 缓存效果

    • 容量未命中(只是太多数据,例如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时效果更好,而且我对MAXSIZE的猜测是正确的,那么我敢打赌它是DRAM页面效果。

快速解释:DRAM具有页面和库的概念。不要与OS页面混淆。 Eac DRAM芯片,因此每个DIMM,有4或8个内部存储体。每个银行可以有一个打开的页面。如果您访问同一页面,同一银行的数据,则速度最快。如果您访问另一个银行中的已读打开页面的数据,速度快但速度低于同一页面相同的银行如果您需要在同一个银行中使用不同的页面,则确实很慢。如果你有一个写回缓存,那么writeack几乎是随机的,所以你可以得到非常糟糕的页面行为。

但是,如果我猜错了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开头的步幅,那么那两个阵列冲突就是你的问题。 / p>

但是,您可能有不同的步幅模式:0,16,32,48 ...在一个数组中,1,17,33,...在另一个数组中。然后不同的阵列对会发生冲突。

-

我没有足够的信息来诊断您的问题。

如果您可以访问性能良好的工具,则可以自己动手。

E.g。在英特尔处理器上,您可以记录我称之为缓存未命中配置文件的理想物理内存地址,计算它们在缓存中映射到的集合,并生成直方图。如果你看到尖峰,这可能是一个问题。同样,您可以生成DRAM页面未命中或库存未命中配置文件。我只提到英特尔,因为我设计了一些硬件来实现这种性能测量。可能应该在ARM上提供同样的东西(如果没有,也许我可以通过销售工具来实现它...... :-))。

如果这些是问题,你怎么解决它?

好吧,通过尝试不同的展示位置,如上所述。这可以帮助实现(缓存集冲突)和DRAM页面问题。

如果步幅有问题,你可能会尝试使数组大小有点不同 - MAXSIZE + 4,MAXSIZE 8等。这可以有效地抵消步幅。 (在超级计算机代码中常见的是查看大小为255或257的数组,以便对抵消跨步访问模式进行相同的重新设计,以免发生冲突。)