我正在尝试实现一种算法来查找文件中一组longs中的最小值和最大值。我的测试文件包含10亿个长。
该算法按预期工作,但执行速度不如天真版本。它应该明显更快,因为天真版本执行大约2n次比较,而此版本执行3n / 2比较。
$ time ./findminmax_naive somelongs
count: 1000000000
min: 0
max: 2147483647
real 0m24.156s
user 0m4.956s
sys 0m3.896s
$ time ./findminmax_faster somelongs
count: 1000000000
min: 0
max: 2147483647
real 0m25.048s
user 0m6.948s
sys 0m3.980s
这是“天真”版本:
#include <alloca.h>
#include <stdio.h>
#include <stdlib.h>
int
main(int ac, char *av[])
{
FILE *f;
long count, readcount, i, min, max;
size_t rlen;
long *n;
if (ac != 2 && ac != 3) {
fprintf(stderr, "Usage: %s <file> [readcount]\n", av[0]);
exit(1);
}
f = fopen(av[1], "r");
if (f == NULL)
perror("fopen");
readcount = 1024;
if (ac == 3)
readcount = atol(av[2]);
n = alloca(sizeof (long) * readcount);
rlen = fread(n, sizeof (*n), 1, f);
min = max = n[0];
count = 1;
while (1) {
rlen = fread(n, sizeof (*n), readcount, f);
for (i = 0; i < (long)rlen; i++) {
count++;
if (n[i] < min)
min = n[i];
if (n[i] > max)
max = n[i];
}
if (feof(f))
break;
}
printf("count: %ld\n", count);
printf("min: %ld\n", min);
printf("max: %ld\n", max);
exit(0);
}
以下是(应该)“更快”版本的代码:
#include <alloca.h>
#include <stdio.h>
#include <stdlib.h>
int
main(int ac, char *av[])
{
FILE *f;
long count, readcount, i, min, max;
size_t rlen;
long *n;
if (ac != 2 && ac != 3) {
fprintf(stderr, "Usage: %s <file> [readcount]\n", av[0]);
exit(1);
}
f = fopen(av[1], "r");
if (f == NULL)
perror("fopen");
readcount = 1024;
if (ac == 3)
readcount = atol(av[2]);
readcount = (readcount + 1) & (-1 << 1);
n = alloca(sizeof (long) * readcount);
rlen = fread(n, sizeof (*n), 1, f);
min = max = n[0];
count = 1;
while (1) {
rlen = fread(n, sizeof (*n), readcount, f);
for (i = 0; i < (long)rlen; i += 2) {
count += 2;
if (n[i] < n[i + 1]) {
if (n[i] < min)
min = n[i];
if (n[i + 1] > max)
max = n[i + 1];
} else {
if (n[i + 1] < min)
min = n[i + 1];
if (n[i] > max)
max = n[i];
}
}
if (feof(f))
break;
}
if (rlen % 2) {
if (n[rlen - 1] < min)
min = n[rlen - 1];
if (n[rlen - 1] > max)
max = n[rlen - 1];
count++;
}
printf("count: %ld\n", count);
printf("min: %ld\n", min);
printf("max: %ld\n", max);
exit(0);
}
你知道我错过了什么吗?
感谢您的帮助。
- Jeremie
答案 0 :(得分:4)
关键是分支预测。除非文件以病态最坏情况顺序排序,否则幼稚版本将执行几乎每次都正确预测的2n个分支。您的“聪明”版本执行几乎从未正确预测的n / 2个分支,以及可能正确预测的额外n个比较。
错误预测的分支成本有多少取决于cpu体系结构甚至特定的cpu模型,但至少我预计错误预测的分支的成本是正确预测分支的几倍。在极端情况下,正确预测的分支可能具有零周期的有效成本。
作为一个有趣的例子,我最近尝试优化strlen
,并发现孤立的非常幼稚的展开strlen
- 一次比较和分支一个字节 - 比聪明的矢量化更快方法。这几乎肯定是因为strlen
具有每个分支的特殊属性,直到最后一个分支始终被正确预测。
顺便说一句,要测试我的假设,请尝试以下输入模式:
999999999,1000000001,999999998,1000000002,999999997,11000000003,...
它将为天真算法提供最坏情况的分支预测,并为您的聪明版本提供外部条件的最佳情况。
答案 1 :(得分:1)
正如@chr所说,“文件I / O会使算法本身的任何优化都相形见绌。”
此外,较少的比较并不等于较少的运行时间消耗。这两种算法的时间复杂度为O(n),忽略了实际的语句成本和抽象 成本。
例如,作为这两种算法的两个粗略框架,时间消耗是程序中所有语句成本的时间。
例如:
//max and min initlaized as 0.
//c1,... reprents the time cost of each instruction.
while(i<count) {//c1
if(a[i]>max) //c2
max = a[i]; //c3
i++; //c4
}
//search of min is like below
时间成本:
T1 = 2n * c1 + 2n * c2 + x * c3 + y * c3 + 2n * c4 = 2n *(c1 + c2 + c4)+(x + y)* c3
其中x和y符合数据的顺序。
而且,(3/2)n的比较,
while(i<count) //c1
if(a[i]<a[i+1]) {//c5
if(a[i]<min) //c2
min = a[i]; //c3
if(a[i+1>max]) //c2
max = a[i+1]; //c3
}
else
...
//same as below,that swap i and i+1
i+=2; //c6
}
时间成本:
T2 = n * c1 + n * c5 + n * 2 * c2 +(x'+ y')* c3 + n * c6 = n *(c1 + c5 + c6)+ 2n * c2 +(x'+ y')* c3
如果max和min是数据的前两个元素,则x = x'= 1; y = y'= 1。
T1-T2 = n * c1 + 2n * c4-n * c5 -n * c6。 为了区分编码器,T1-T2可能不同。
更复杂的是x,y,x',y'是可变的,但我不会进一步讨论。我的目的是显示真正的运行时间成本。
如果您在阅读上述内容后仍感到困惑,则应阅读算法导论的第2.2章。
答案 2 :(得分:0)
我可以想到两件事:
答案 3 :(得分:0)
首先,请原谅我在一个答案中回答所有问题。我知道我不应该在stackoverflow.com上这样做,但鉴于不同的主题或多或少交织在一起,这样就更容易了。
所以,这是我现在用来测试算法的代码。 与以前版本的差异:
以下是代码:
#include <sys/mman.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define ROUNDUP(x, y) ((((x) + ((y) - 1)) / (y)) * (y))
#define USCARRY(x) ((x) < 0 ? (x) + 1000000 : (x))
int
main(int ac, char *av[])
{
struct stat st;
struct timeval start, end;
long count, min, max, pgsiz;
long *n, *p, *endp;
int fd, warmup;
if (ac != 3 && ac != 4) {
fprintf(stderr, "Usage: %s <\"trivial\"|\"faster\"> "
"<file> [warmup]\n", av[0]);
exit(1);
}
fd = open(av[2], O_RDONLY);
if (fd == -1)
perror("open");
fstat(fd, &st);
pgsiz = sysconf(_SC_PAGESIZE);
n = mmap(NULL, ROUNDUP(st.st_size, pgsiz), PROT_READ,
MAP_SHARED, fd, 0);
if (n == MAP_FAILED)
perror("mmap");
warmup = 0;
if (ac == 4)
warmup = atoi(av[3]);
// warm up the filesystem cache
count = st.st_size / sizeof (*n);
endp = &n[count - 1];
//printf("%zu\n", sizeof (*p));
//printf("%zu\n", sizeof (count));
//exit(0);
if (warmup) {
for (p = n; p <= endp; p++) {
fwrite(p, sizeof (*p), 1, stdout);
min = *p;
}
}
// start algorithm
gettimeofday(&start, NULL);
if (!strcmp(av[1], "trivial")) {
min = max = n[0];
p = &n[1];
while (p <= endp) { // c1 * n
if (p[0] < min) // c2 * n
min = p[0]; // c3 * x
if (p[0] > max) // c2 * n
max = p[0]; // c3 * y
p++; // c4 * n
}
} else if (!strcmp(av[1], "faster")) {
min = max = n[0];
p = &n[1];
while (p < endp) { // c1 * n/2
if (p[0] < p[1]) { // c2 * n/2
if (p[0] < min) // c2 * n/4
min = p[0]; // c3 * x/2
if (p[1] > max) // c2 * n/4
max = p[1]; // c3 * y/2
} else {
if (p[1] < min) // c2 * n/4
min = p[1]; // c3 * x/2
if (p[0] > max) // c2 * n/4
max = p[0]; // c3 * y/2
}
p += 2; // c5 * n
}
if (p == endp) {
if (*endp < min)
min = *endp;
else if (*endp > max)
max = *endp;
}
} else {
printf("sorry\n");
exit(1);
}
gettimeofday(&end, NULL);
printf("time: %ld.%ld\n", end.tv_sec - start.tv_sec,
USCARRY(end.tv_usec - start.tv_usec));
printf("count: %ld\n", count);
printf("min: %ld\n", min);
printf("max: %ld\n", max);
exit(0);
}
以下是我用于测试用例的文件:
$ ls -l _*
-rw-r--r-- 1 jlh jlh 2400000000 May 27 23:37 _bestcase
-rw-r--r-- 1 jlh jlh 2400000000 May 27 08:40 _random
-rw-r--r-- 1 jlh jlh 2400000000 May 27 23:38 _worstcase
$ od -N 64 -t dL _bestcase
0000000 0 300000000
0000020 1 299999999
0000040 2 299999998
0000060 3 299999997
0000100
$ od -N 64 -t dL _random
0000000 8600270969454287374 8436406000964321037
0000020 7348498334162074664 2050332610730417325
0000040 8183771519386464726 4134520779774161130
0000060 2045475555785071180 2859007060406233926
0000100
$ od -N 64 -t dL _worstcase
0000000 150000000 150000000
0000020 149999999 150000001
0000040 149999998 150000002
0000060 149999997 150000003
0000100
好的,首先让我们预热缓存并确认没有丢失的页面然后可能搞砸了结果:
$ ./findminmax trivial _random
time: 3.543573
count: 300000000
min: 31499144704
max: 9223372004409096866
$ ./findminmax trivial _random
time: 1.466323
count: 300000000
min: 31499144704
max: 9223372004409096866
$ perf stat -e minor-faults,major-faults ./findminmax trivial _random
time: 1.284729
count: 300000000
min: 31499144704
max: 9223372004409096866
Performance counter stats for './findminmax trivial _random':
586,066 minor-faults
0 major-faults
1.350118552 seconds time elapsed
因为你可以看到没有重大的页面错误。从现在开始,我们可以认为不会产生I / O影响。 2.指令计数
@ H2CO3,@ vvy,你对其他指令也很重要的事实是完全正确的(我会考虑每个操作在这里占用相同数量的CPU周期,并且会活动appart CPU cache miss)。我不知道为什么到目前为止我读到的有关算法的文章只关注比较的数量(好吧,我承认我没有读过很多文章;)。
我已经用自己的平均情况估算了循环中的每一步(我认为最坏的情况在这里并不有趣),这对你来说略有不同。
如果我没弄错的话: - 对于平凡版本:n *(c1 + 2 * c2 + c4)+(x + y)* c3 - 对于更快的版本:n / 2 *(c1 + 3 * c2 + c5)+(x + y)* c3
现在在我的理解中,很难进一步说明每个cN需要多少CPU周期,因为它因CPU而异。
让我们检查一下我的计算机上发生了多少指令,分支和分支未命中,这些指令大多处于空闲状态,而每个算法都在每个测试用例上执行,并带有一个热缓存(请注意,我测试了每个案例3次以验证没有重大偏差):
$ perf stat -e branches,branch-miss,instructions ./findminmax_stackoverflow trivial _random
time: 1.547087
count: 300000000
min: 31499144704
max: 9223372004409096866
Performance counter stats for './findminmax_stackoverflow trivial _random':
1,083,101,126 branches
52,388 branch-miss
4,335,175,257 instructions # 0.00 insns per cycle
1.623851849 seconds time elapsed
$ perf stat -e branches,branch-miss,instructions ./findminmax_stackoverflow faster _random
time: 2.748967
count: 300000000
min: 31499144704
max: 9223372004409096866
Performance counter stats for './findminmax_stackoverflow faster _random':
783,120,927 branches
75,063,008 branch-miss
3,735,286,264 instructions # 0.00 insns per cycle
1.824884443 seconds time elapsed
请注意,对于速度较快的版本,我们的指令较少,但运行起来要花费的时间要长得多,但可能因为有更多的分支未命中,按顺序或数量而退出!
$ perf stat -e branches,branch-miss,instructions ./findminmax_stackoverflow trivial _bestcase
time: 1.267697
count: 300000000
min: 0
max: 300000000
Performance counter stats for './findminmax_stackoverflow trivial _bestcase':
1,082,801,759 branches
49,802 branch-miss
4,334,200,448 instructions # 0.00 insns per cycle
1.343425753 seconds time elapsed
$ perf stat -e branches,branch-miss,instructions ./findminmax_stackoverflow faster _bestcase
time: 0.957440
count: 300000000
min: 0
max: 300000000
Performance counter stats for './findminmax_stackoverflow faster _bestcase':
782,844,232 branches
49,768 branch-miss
3,734,103,167 instructions # 0.00 insns per cycle
1.035189822 seconds time elapsed
$ perf stat -e branches,branch-miss,instructions ./findminmax_stackoverflow trivial _worstcase
time: 7.860047
count: 300000000
min: 1
max: 299999999
Performance counter stats for './findminmax_stackoverflow trivial _worstcase':
1,490,947,270 branches
2,127,876 branch-miss
7,159,600,607 instructions # 0.00 insns per cycle
6.916856158 seconds time elapsed
$ perf stat -e branches,branch-miss,instructions ./findminmax_stackoverflow faster _worstcase
time: 7.616476
count: 300000000
min: 1
max: 299999999
Performance counter stats for './findminmax_stackoverflow faster _worstcase':
1,196,744,455 branches
2,024,250 branch-miss
6,594,182,002 instructions # 0.00 insns per cycle
6.675068846 seconds time elapsed
所以,非常有趣的是,“随机”情况实际上比最坏情况更快,但没有显示出太大差异。我看到的唯一区别是我的最坏情况包含“小”数字(可以用32位编码),而随机情况包含真正的64位数字。
让我们试试一组“小”随机数(仍以64位存储编码):
$ od -N 64 -t dL _randomsmall
0000000 1418331637 2076047555
0000020 22077878 1677970822
0000040 1845481621 609558726
0000060 1668260452 335112094
0000100
$ perf stat -e branches,branch-miss,instructions ./findminmax_stackoverflow trivial _randomsmall
time: 7.682443
count: 300000000
min: 9
max: 2147483647
Performance counter stats for './findminmax_stackoverflow trivial _randomsmall':
1,481,062,942 branches
2,564,853 branch-miss
6,223,311,378 instructions # 0.00 insns per cycle
6.739897078 seconds time elapsed
$ perf stat -e branches,branch-miss,instructions ./findminmax_stackoverflow faster _randomsmall
time: 7.772994
count: 300000000
min: 9
max: 2147483647
Performance counter stats for './findminmax_stackoverflow faster _randomsmall':
1,177,042,675 branches
77,686,346 branch-miss
5,607,194,799 instructions # 0.00 insns per cycle
6.834074812 seconds time elapsed
因此,正如我猜测的那样,小数字实际上比大数字更慢,即使它们都包含64位字。有很多分支未命中的“小”数字,原因可能只有CPU设计师能够说出: - )。
另一个值得注意的事情是,perf(1)测量的经过时间通常小于程序本身测量的经过时间。我认为这可以通过程序本身使用实时的事实来解释,而perf(1)使用处理时间(流程实际运行的时间)。我尝试使用getrusage(2),但是我到达这里的时间不匹配(例如我将1.6s作为用户时间,1.4s作为系统时间,而perf(1)测量6.8s)。
所以,如果你走得那么远,谢谢:)。我很抱歉,所有这些实验都只能得出含糊不清的结论。希望开明的读者会对此有所了解:)。
- Jeremie