优化索引数组求和

时间:2010-12-04 20:07:27

标签: c++

我有以下C ++代码:

const int N = 1000000
int id[N]; //Value can range from 0 to 9
float value[N];

// load id and value from an external source... 

int size[10] = { 0 };
float sum[10] = { 0 };
for (int i = 0; i < N; ++i)
{
    ++size[id[i]];
    sum[id[i]] += value[i];
}

我应该如何优化循环?

我考虑使用SSE将每4个浮点数添加到一个和,然后在N次迭代之后,总和只是xmm寄存器中4个浮点数的总和,但是当源被索引像这样并且需要时这不起作用写出10个不同的数组。

6 个答案:

答案 0 :(得分:2)

使用SIMD指令很难优化这种循环。在大多数SIMD指令集中,不仅没有一种简单的方法可以执行这种索引读取(“聚集”)或写入(“分散”),即使有,这个特定的循环仍然存在您可能有的问题两个值映射到一个SIMD寄存器中的相同id,例如当

id[0] == 0
id[1] == 1
id[2] == 2
id[3] == 0

在这种情况下,显而易见的方法(这里是伪代码)

x = gather(size, id[i]);
y = gather(sum, id[i]);
x += 1; // componentwise
y += value[i];
scatter(x, size, id[i]);
scatter(y, sum, id[i]);

也不起作用!

如果存在极少数可能的情况(例如,假设sumsize每个只有3个元素),你可以通过仅进行强力比较,但这不是真的是规模。

在不使用SIMD的情况下更快地实现这一点的一种方法是使用展开来分解指令之间的依赖关系:

int size[10] = { 0 }, size2[10] = { 0 };
int sum[10] = { 0 }, sum2[10] = { 0 };
for (int i = 0; i < N/2; i++) {
  int id0 = id[i*2+0], id1 = id[i*2+1];
  ++size[id0];
  ++size2[id1];
  sum[id0] += value[i*2+0];
  sum2[id1] += value[i*2+1];
}

// if N was odd, process last element
if (N & 1) {
  ++size[id[N]];
  sum[id[N]] += value[N];
}

// add partial sums together
for (int i = 0; i < 10; i++) {
  size[i] += size2[i];
  sum[i] += sum2[i];
}

这是否有帮助取决于目标CPU。

答案 1 :(得分:1)

好吧,你在循环中调用id [i]两次。如果您愿意,可以将其存储在变量或寄存器int中。

register int index;
for(int i = 0; i < N; ++i)
{
index = id[i];
++size[index];
sum[index] += value[i];
}

MSDN文档说明了注册:

  

register关键字指定了   变量将存储在一个   机器注册..微软特定

     

编译器不接受用户   请求寄存器变量;   相反,它自己注册   全球时的选择   寄存器分配优化(/ Oe   选项)已启用。但是,所有其他   与寄存器相关的语义   关键字很荣幸。

答案 2 :(得分:0)

您可以做的就是使用-S标志(或等效,如果您不使用gcc)进行编译,并使用-O-O2比较各种汇编输出, -O3个标志。优化循环的一种常见方法是进行一定程度的展开,对于(一个非常简单,天真)的例子:

int end = N/2;
int index = 0;
for (int i = 0; i < end; ++i)
{
    index = 2 * i;
    ++size[id[index]];
    sum[id[index]] += value[index];
    index++;
    ++size[id[index]];
    sum[id[index]] += value[index];
}

会将cmp个指令的数量减少一半。但是,任何半好的优化编译器都会为您完成此任务。

答案 3 :(得分:0)

你确定它会有很大的不同吗?可能是“加载来自外部源的id”将比将值加起来要长得多。

在知道瓶颈在哪里之前不要进行优化。

编辑回答评论:你误解了我。如果从硬盘加载id需要10秒钟,那么在处理列表上花费的一秒钟的分数在更宏大的方案中是无关紧要的。让我们说加载需要10秒钟,处理需要1秒钟:

你优化了处理循环,所以需要0秒(几乎不可能,但它用来说明一点)然后它仍然需要10秒钟。 11秒实际上并不是性能损失,你最好将优化时间集中在实际的数据负载上,因为这很可能是缓慢的部分。

实际上,进行双缓冲数据加载可能非常理想。即你加载缓冲区0,然后你开始加载缓冲区1.缓冲区1正在加载你进程缓冲区0.当完成后,在处理缓冲区1时启动下一个缓冲区的加载,依此类推。通过这种方式,您可以完全分摊处理成本。

进一步编辑:实际上,您最好的优化可能来自将事物加载到一组桶中,从而消除了计算的“id [i]”部分。然后,您可以简单地卸载到3个线程,其中每个线程使用SSE添加。通过这种方式,您可以将它们全部同时运行,并且如果您至少拥有三核计算机,则可以在十分之一的时间内处理整个数据。组织数据以实现最佳处理将始终允许最佳优化,IMO。

答案 4 :(得分:0)

根据您的目标计算机和编译器,查看您是否拥有_mm_prefetch内在函数并尝试一下。回到Pentium D时代,只要您在需要数据之前预先进行一些循环迭代,就可以使用asm指令为该内在函数预取数据,这是一个真正的速度获胜。

有关英特尔的更多信息,请参阅here(PDF中的第95页)。

答案 5 :(得分:0)

这种计算可以简单地并行化;只需添加

#pragma omp parallel_for reduction(+:size,+:sum)schedule(static)

如果你有OpenMP支持(在GCC中使用-fopenmp),那么

就在循环的正上方。但是,我不希望在典型的多核台式机上加速很快;你每个项目的计算量很少,你几乎肯定会受到内存带宽的限制。

如果你需要为给定的id映射多次执行求和(即value []数组的变化比id []更频繁),你可以通过将value []元素预先排序到你的内存带宽需求来减半id命令并从id []中删除每个元素的提取:

for(i = 0,j = 0,k = 0; j <10; sum [j] + = tmp,j ++)

for(k + = size [j],tmp = 0; i&lt; k; i ++)

  tmp += value[i];