只有一种方法可以优化代码:弄清楚你在做什么很慢,然后少做。“少做事”的一个特例是做其他事情,而不是更快。
所以首先,这是我根据您发布的代码所做的事情:
#include <fstream>
#include <sstream>
using std::ios_base;
template<typename Iterator, typename Value>
void iota(Iterator start, Iterator end, Value val) {
while (start != end) {
*(start++) = val++;
}
}
int main() {
const int dim = 1000;
const int cubesize = dim*dim*dim;
const int squaresize = dim*dim;
const int steps = 7; //ranges from 1 to 255
typedef unsigned char uchar;
uchar *partMap = new uchar[cubesize];
// dummy data. I timed this separately and it takes about
// a second, so I won't worry about its effect on overall timings.
iota(partMap, partMap + cubesize, uchar(7));
uchar *projection = new uchar[squaresize];
for (int stage = 1; stage < steps; stage++) {
for (int j = 0; j < dim; j++) {
for (int i = 0; i < dim; i++)
{
int sum = 0;
for (int k = 0; k < dim; k++)
if (partMap[(((i * dim) + k) * dim) + j] >= stage)
sum++;
projection[(j*dim) + i] = sum;
}
}
std::stringstream filename;
filename << "results" << stage << ".bin";
std::ofstream file(filename.str().c_str(),
ios_base::out | ios_base::binary | ios_base::trunc);
file.write((char *)projection, squaresize);
}
delete[] projection;
delete[] partMap;
}
(编辑:刚刚注意到“投影”应该是一个int数组,而不是uchar。我的错。这会对一些时间产生影响,但希望不会太大。)
然后我复制result*.bin
到gold*.bin
,所以我可以检查我未来的变化如下:
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 1m41.978s
user 1m39.450s
sys 0m0.451s
好的,现在是 100 秒。
因此,推测它正在通过缓慢的十亿项数据数组,让我们尝试只通过一次,而不是每个阶段一次:
uchar *projections[steps];
for (int stage = 1; stage < steps; stage++) {
projections[stage] = new uchar[squaresize];
}
for (int j = 0; j < dim; j++) {
for (int i = 0; i < dim; i++)
{
int counts[256] = {0};
for (int k = 0; k < dim; k++)
counts[partMap[(((i * dim) + k) * dim) + j]]++;
int sum = 0;
for (int idx = 255; idx >= steps; --idx) {
sum += counts[idx];
}
for (int stage = steps-1; stage > 0; --stage) {
sum += counts[stage];
projections[stage][(j*dim) + i] = sum;
}
}
}
for (int stage = 1; stage < steps; stage++) {
std::stringstream filename;
filename << "results" << stage << ".bin";
std::ofstream file(filename.str().c_str(),
ios_base::out | ios_base::binary | ios_base::trunc);
file.write((char *)projections[stage], squaresize);
}
for (int stage = 1; stage < steps; stage++) delete[] projections[stage];
delete[] partMap;
它有点快:
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 1m15.176s
user 1m13.772s
sys 0m0.841s
现在,steps
在这个例子中非常小,所以我们对“counts”数组做了很多不必要的工作。甚至没有分析,我猜测与计数到 1000(沿着我们的列运行)相比,计数到 256 两次(一次用于清除数组,一次用于求和)非常重要。所以让我们改变一下:
for (int j = 0; j < dim; j++) {
for (int i = 0; i < dim; i++)
{
// steps+1, not steps. I got this wrong the first time,
// which at least proved that my diffs work as a check
// of the answer...
int counts[steps+1] = {0};
for (int k = 0; k < dim; k++) {
uchar val = partMap[(((i * dim) + k) * dim) + j];
if (val >= steps)
counts[steps]++;
else counts[val]++;
}
int sum = counts[steps];
for (int stage = steps-1; stage > 0; --stage) {
sum += counts[stage];
projections[stage][(j*dim) + i] = sum;
}
}
}
现在我们只使用实际需要的桶数。
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 0m27.643s
user 0m26.551s
sys 0m0.483s
欢呼。该代码的速度几乎是第一个版本的 4 倍,并且产生了相同的结果。我所做的只是改变数学运算的顺序:我们甚至还没有研究多线程或预取。而且我没有尝试过任何高度技术性的循环优化,只是把它留给了编译器。所以这可以被认为是一个不错的开始。
然而,它仍然比 iota 运行的 1 长一个数量级。所以可能还有很大的收获。一个主要区别是 iota 按顺序在 1d 数组上运行,而不是到处跳跃。正如我在第一个答案中所说,您的目标应该是始终在多维数据集上使用顺序。
所以,让我们进行一行更改,切换 i 和 j 循环:
for (int i = 0; i < dim; i++)
for (int j = 0; j < dim; j++) {
这仍然不是顺序,但这确实意味着我们一次只关注一百万字节的多维数据集。现代 CPU 至少有 4MB 高速缓存,所以如果运气好的话,我们只会在整个程序中为多维数据集的任何给定部分访问主内存一次。有了更好的局部性,我们也可以减少进出 L1 缓存的流量,但主内存是最慢的。
它有多大的不同?
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 0m8.221s
user 0m4.507s
sys 0m0.514s
不错。实际上,仅此更改就将原始代码从 100 秒变为 20 秒。所以这是 5 的原因,而我所做的其他一切都是 5 的原因(我认为上面“用户”和“实时”之间的差异主要是由于我的病毒扫描程序是运行,它不是更早。“用户”是程序占用 CPU 的时间,“真实”包括暂停所花费的时间,等待 I/O 或给另一个进程运行时间)。
当然,我的桶排序依赖于这样一个事实,即我们对每列中的值所做的任何事情都是可交换和关联的。减少存储桶的数量仅起作用,因为大值都被视为相同。这可能不适用于您的所有操作,因此您必须依次查看每个操作的内部循环以弄清楚如何处理它。
而且代码有点复杂。我们不是在每个阶段运行“废话”的数据,而是在一次对数据的运行中同时计算所有阶段。如果您按照我在第一个答案中的建议,一次性开始进行行和列计算,情况会变得更糟。您可能必须开始将代码分解为函数以保持可读性。
最后,我的很多性能提升来自对“步数”很小这一事实的优化。steps=100
,我得到:
$ make big -B CPPFLAGS="-O3 -pedantic -Wall" && time ./big; for n in 1 2 3 4 5
6; do diff -q results$n.bin gold$n.bin; done
g++ -O3 -pedantic -Wall big.cpp -o big
real 0m22.262s
user 0m10.108s
sys 0m1.029s
这还不错。使用 steps=100 时,原始代码可能需要大约 1400 秒,尽管我不会运行它来证明这一点。但值得记住的是,我并没有完全消除对“步骤”的时间依赖性,只是让它成为亚线性的。