我正在尝试优化一个小的,高度使用的函数,该函数使用无符号short int中的高位来指示要一起求和的数组值。起初我使用下面显示的明显方法。请注意,循环展开未明确显示,因为它应由编译器完成。
int total = 0;
for(unsigned short mask = 0x0001, j = 0; mask != 0; mask <<= 1, j++){
if (i & mask){
total += value[j];
}
}
但是,后来我认为删除分支以帮助CPU流水线操作可能会更好,并提出以下建议。
int total = 0;
for(unsigned short mask = 0x0001, j = 0; mask != 0; mask <<= 1, j++){
total += ((i & mask) != 0) * value[j];
}
请注意,由于(i&amp; mask)不会产生布尔答案,因此与0的比较会强制结果为1或0.虽然第二种方法从代码的这一部分中删除了if语句,除了等式的其余部分之外,第二个解决方案需要在每次迭代时运行0或1的乘法。
哪个代码运行得更快?
答案 0 :(得分:13)
哪个代码运行得更快?
测试它以找出答案。
另外,请查看编译器发出的代码的汇编语言版本,因为您可能会看到其中的内容让您感到惊讶,并提示进一步优化(例如,使用short
作为您正在使用可能需要使用机器的自然整数大小的更多指令。
答案 1 :(得分:9)
要么更快。对于某些处理器,实际输入数据可能会改变答案。您需要使用实际数据来分析这两种方法。以下是一些可能影响x86硬件实际性能的事情。
让我们假设您正在使用新型Pentium 4.该处理器在CPU中有两级分支预测器。如果分支预测器可以正确猜出分支方向,我怀疑第一个将是最快的。如果标志几乎都是相同的值,或者如果它们在大多数时间以非常简单的模式交替,则最有可能发生这种情况。如果标志是真正随机的,那么分支预测器将在一半时间内出错。对于我们假设的32阶段奔腾4,这将扼杀性能。对于Pentium 3芯片,Core 2芯片,Core i7和大多数AMD芯片,流水线较短,因此坏分支预测的成本要低得多。
如果您的值向量明显大于处理器的缓存,则任何一种方法都将受到内存带宽的限制。它们都具有基本相同的性能特征。如果值向量适合缓存,请注意如何进行任何分析,以便其中一个测试循环不会因填充缓存而受到惩罚而另一个受益于它。
答案 2 :(得分:7)
如果没有乘法,你可以使它无分支。看起来对于每个位集,您使用该位位置作为数组的索引。
首先,您可以轻松提取设置为:
的位unsigned short set_mask= i & -i;
i&= i - 1;
然后,您可以通过计算(set_mask - 1)
中设置的位来获取位索引。这是一个恒定的时间公式。
某些平台也有一个内在函数来获取位集的位索引,这可能更快。 x86有bsr
,PPC有cntlz
。
所以答案是无分支无乘法版本可能是最快的:)
答案 3 :(得分:4)
这次修订怎么样?
int total = 0;
for (unsigned mask = i & 0xFFFF, j = 0; mask != 0; mask >>= 1, j++){
total += (mask & 0x0001) * value[j];
}
我已将mask
放入i
限制为16位无符号范围的副本中,但代码会检查是否设置了掩码的最后一位,将数组值乘以该位。这应该更快,因为每次迭代的操作更少,并且只需要主循环分支和条件。此外,如果i
很小,那么循环可以提前退出。
这证明了为什么测量很重要。我正在使用过时的Sun SPARC。我写了一个测试程序如图所示,问题中的两个竞争者是测试0和测试1,我自己的答案是测试2.然后运行时序测试。 'sum'打印为完整性检查 - 以确保算法都给出相同的答案。
64位未经优化:
gcc -m64 -std=c99 -I$HOME/inc -o x x.c -L$HOME/lib/sparcv9 -ljl -lposix4
Test 0: (sum = 1744366) 7.973411 us
Test 1: (sum = 1744366) 10.269095 us
Test 2: (sum = 1744366) 7.475852 us
很好:我的速度比原版略快,加强版的速度也慢了。
64位优化:
gcc -O4 -m64 -std=c99 -I$HOME/inc -o x x.c -L$HOME/lib/sparcv9 -ljl -lposix4
Test 0: (sum = 1744366) 1.101703 us
Test 1: (sum = 1744366) 1.915972 us
Test 2: (sum = 1744366) 2.575318 us
Darn - 我的版本现在是最慢的。优化器很好!
32位优化:
gcc -O4 -std=c99 -I$HOME/inc -o x x.c -L$HOME/lib -ljl -lposix4
Test 0: (sum = 1744366) 0.839278 us
Test 1: (sum = 1744366) 1.905009 us
Test 2: (sum = 1744366) 2.448998 us
32位未经优化:
gcc -std=c99 -I$HOME/inc -o x x.c -L$HOME/lib -ljl -lposix4
Test 0: (sum = 1744366) 7.493672 us
Test 1: (sum = 1744366) 9.610240 us
Test 2: (sum = 1744366) 6.838929 us
相同的代码(32位)Cygwin和一个不那么老练的笔记本电脑(32位,优化)
Test 0: (sum = 1744366) 0.557000 us
Test 1: (sum = 1744366) 0.553000 us
Test 2: (sum = 1744366) 0.403000 us
现在我的代码 最快。这就是你测量的原因!它还说明为什么那些以生活为基准的人会感到心烦意乱。
测试工具(如果您需要timer.h
和timer.c
代码,请大声喊叫):
#include <stdio.h>
#include "timer.h"
static volatile int value[] =
{
12, 36, 79, 21, 31, 93, 24, 15,
56, 63, 20, 47, 62, 88, 9, 36,
};
static int test_1(int i)
{
int total = 0;
for (unsigned short mask = 0x0001, j = 0; mask != 0; mask <<= 1, j++)
{
if (i & mask)
total += value[j];
}
return(total);
}
static int test_2(int i)
{
int total = 0;
for (unsigned short mask = 0x0001, j = 0; mask != 0; mask <<= 1, j++)
{
total += ((i & mask) != 0) * value[j];
}
return(total);
}
static int test_3(int i)
{
int total = 0;
for (unsigned mask = i & 0xFFFF, j = 0; mask != 0; mask >>= 1, j++)
{
total += (mask & 0x0001) * value[j];
}
return(total);
}
typedef int(*func_pointer)(int);
static func_pointer test[] = { test_1, test_2, test_3 };
#define DIM(x)(sizeof(x)/sizeof(*(x)))
int main()
{
int i, j, k;
char buffer[32];
for (i = 0; i < DIM(test); i++)
{
Clock t;
long sum = 0;
clk_init(&t);
clk_start(&t);
for (j = 0; j < 0xFFFF; j += 13)
{
int rv;
for (k = 0; k < 1000; k++)
rv = (*test[i])(j);
sum += rv;
}
clk_stop(&t);
printf("Test %d: (sum = %ld) %9s us\n", i, sum,
clk_elapsed_us(&t, buffer, sizeof(buffer)));
}
}
我没有花时间研究为什么我的代码在优化时会变慢。
答案 4 :(得分:3)
这取决于编译器,机器指令集上的完全,可能还有月相。
因此没有具体的正确答案。如果您真的想知道,请检查编译器的汇编输出。
从简单的角度来看,我会说第二个是慢的,因为它涉及第一个加乘法的所有计算。但编译器可能足够聪明,可以将其优化掉。
所以正确的答案是:它取决于。
答案 5 :(得分:1)
虽然第二个示例没有显式分支,但可能会有一个隐式的将比较结果转换为bool。您可以通过打开编译器的汇编列表输出并查看它来获得一些见解。
当然,唯一可以确切知道的方法是两种方式。
答案 6 :(得分:1)
答案肯定是:在目标硬件上试一试,看看。并且请务必遵循过去几周内发布在SO上的众多微基准/秒表基准问题的建议。
链接到一个基准测试问题:Is stopwatch benchmarking acceptable?
就个人而言,我会选择if,除非有一个非常令人信服的理由使用“混淆”的替代方案。
答案 7 :(得分:1)
确定陈述真实性的唯一真正方法是测试。考虑到这一点,我会同意以前的帖子说试试吧!
在大多数现代处理器上,分支是一个代价高昂的过程,尤其是不经常分支的分支。这是因为必须刷新管道,导致CPU无法同时尝试执行一个或多个指令 - 只是因为它不知道下一条指令的来源。通过一些分支,可能的控制流变得复杂,CPU可以同时尝试所有可能性,因此必须执行分支,然后在此之后立即开始执行许多指令。
答案 8 :(得分:1)
为什么不这样做(假设我是32位)
for (i2 = i; i2; i2 = i3) {
i3 = i2 & (i2-1);
last_bit = i2-i3;
a = last_bit & 0xffff;
b = (last_bit << 16);
j = place[a] + big_place[b];
total += value[j];
}
where place是一张大小为2 ^ 15 + 1的表格 place [0] = 0,place [1] = 1,place [2] = 2,place [4] = 3,place [8] = 4 ... place [15] = 16(其余值为don'无所谓)。和big_place几乎完全相同: big_place [0] = 0,big_place [1] = 17 .... big_place [15] = 32。
答案 9 :(得分:1)
尝试
total += (-((i & mask) != 0)) & value[j];
取代
total += ((i & mask) != 0) * value[j];
这避免了繁殖。是否存在分支取决于编译器是否足够聪明以找到 - (foo!= 0)的无分支代码。 (这是可能的,但我会有点惊讶。)
(当然,这取决于二进制补码表示; C标准对此不可知。)
您可以像这样帮助编译器,假设32位整数和签名&gt;&gt;传播符号位:
total += (((int)((i & mask) << (31 - j))) >> 31) & value[j];
也就是说,将可能设置的位移到最重要的位置,转换为signed int,然后一直向右移回最不重要的位置,在上面的实现中产生全0或全1 - 定义的假设。 (我没有测试过这个。)
另一种可能性:一次考虑(例如)4位的块。有16种不同的加法序列;您可以为每个代码块调度到展开的代码,而在每个代码块中根本不进行任何测试。这里的希望是间接跳转到成本不到4个测试和分支。
更新:使用Jonathan Leffler的脚手架,我的MacBook上的 4位一次性方法最快。否定 - 并且结果与乘法相同。我想知道处理器是否会更快地将0和1等特殊情况相乘(或者如果对于大多数位清除或大多数位设置的被乘数更快,则不会出现这种特殊情况)。
我没有对接受的答案进行编码,因为它不可能在这个特定的基准测试中获得最快的速度(它应该只从枚举设置位中获得大部分好处,在稀疏集上做得最好,但完全一半的位是设置在这个基准)。以下是我对Leffler代码的更改,以防其他任何人出于奇怪的动机花时间在此:
#include <stdio.h>
#include <time.h>
static int value[] =
{
12, 36, 79, 21, 31, 93, 24, 15,
56, 63, 20, 47, 62, 88, 9, 36,
};
static int test_1(int i)
{
int total = 0;
for (unsigned short mask = 0x0001, j = 0; mask != 0; mask <<= 1, j++)
{
if (i & mask)
total += value[j];
}
return(total);
}
static int test_2(int i)
{
int total = 0;
for (unsigned short mask = 0x0001, j = 0; mask != 0; mask <<= 1, j++)
{
total += ((i & mask) != 0) * value[j];
}
return(total);
}
static int test_3(int i)
{
int total = 0;
for (unsigned mask = i & 0xFFFF, j = 0; mask != 0; mask >>= 1, j++)
{
total += (mask & 0x0001) * value[j];
}
return(total);
}
static int test_4(int i)
{
int total = 0;
for (unsigned mask = i & 0xFFFF, j = 0; mask != 0; mask >>= 1, j++)
{
total += -(mask & 0x0001) & value[j];
}
return(total);
}
static int test_5(int i)
{
int total = 0;
const int *p = value;
for (unsigned mask = i & 0xFFFF; mask != 0; mask >>= 4, p += 4)
{
switch (mask & 0xF)
{
case 0x0: break;
case 0x1: total += p[0]; break;
case 0x2: total += p[1]; break;
case 0x3: total += p[1] + p[0]; break;
case 0x4: total += p[2]; break;
case 0x5: total += p[2] + p[0]; break;
case 0x6: total += p[2] + p[1]; break;
case 0x7: total += p[2] + p[1] + p[0]; break;
case 0x8: total += p[3]; break;
case 0x9: total += p[3] + p[0]; break;
case 0xA: total += p[3] + p[1]; break;
case 0xB: total += p[3] + p[1] + p[0]; break;
case 0xC: total += p[3] + p[2]; break;
case 0xD: total += p[3] + p[2] + p[0]; break;
case 0xE: total += p[3] + p[2] + p[1]; break;
case 0xF: total += p[3] + p[2] + p[1] + p[0]; break;
}
}
return(total);
}
typedef int(*func_pointer)(int);
static func_pointer test[] = { test_1, test_2, test_3, test_4, test_5 };
#define DIM(x)(sizeof(x)/sizeof(*(x)))
int main()
{
int i, j, k;
for (i = 0; i < DIM(test); i++)
{
long sum = 0;
clock_t start = clock();
for (j = 0; j <= 0xFFFF; j += 13)
{
int rv;
for (k = 0; k < 1000; k++)
rv = (*test[i])(j);
sum += rv;
}
clock_t stop = clock();
printf("(sum = %ld) Test %d: %8.6f s\n", sum, i + 1,
(stop - start) / (1.0 * CLOCKS_PER_SEC));
}
}
结果(gcc -O4 -std=c99 branchmult2.c
):
(sum = 1744366) Test 1: 0.225497 s
(sum = 1744366) Test 2: 0.221127 s
(sum = 1744366) Test 3: 0.126301 s
(sum = 1744366) Test 4: 0.124750 s
(sum = 1744366) Test 5: 0.064877 s
编辑2:我认为如果没有volatile
限定符,测试会更加真实。
答案 10 :(得分:1)
为了更快,你可以避免循环,移位和乘法 - 使用开关。
switch (i) {
case 0: break;
case 1: total = value[0]; break;
case 2: total = value[1]; break;
case 3: total = value[1] + value[0]; break;
case 4: total = value[2]; break;
case 5: total = value[2] + value[0]; break;
...
}
输入很多,但我想它在运行时会更快。你无法击败查找表的性能!
我宁愿写一个小的Perl脚本来为我生成这段代码 - 只是为了避免输入错误。
如果您认为它有点极端,您可以使用较小的表 - 对于4位,并进行多次查找,每次都移动掩码。性能会受到影响,但代码会小得多。
答案 11 :(得分:1)
明显的解决方案:
int total = 0;
for(unsigned j = 0; j < 16; j++){
total += -(i>>j & 1) & value[j];
}