我有一个带内循环的简单函数 - 它缩放输入值,在查找表中查找输出值,并将其复制到目标。 (ftol_ambient是我从网上复制的一种技巧,用于将float快速转换为int)。
for (i = 0; i < iCount; ++i)
{
iScaled = ftol_ambient(*pSource * PRECISION3);
if (iScaled <= 0)
*pDestination = 0;
else if (iScaled >= PRECISION3)
*pDestination = 255;
else
{
iSRGB = FloatToSRGBTable3[iScaled];
*pDestination = iSRGB;
}
pSource++;
pDestination++;
}
现在我的查找表是有限的,并且浮点数是无限的,因此有可能出现一个错误。我用一些代码创建了一个函数副本来处理这种情况。请注意,唯一的区别是添加了2行代码 - 请忽略丑陋的指针转换。
for (i = 0; i < iCount; ++i)
{
iScaled = ftol_ambient(*pSource * PRECISION3);
if (iScaled <= 0)
*pDestination = 0;
else if (iScaled >= PRECISION3)
*pDestination = 255;
else
{
iSRGB = FloatToSRGBTable3[iScaled];
if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))
++iSRGB;
*pDestination = (unsigned char) iSRGB;
}
pSource++;
pDestination++;
}
这是奇怪的部分。我正在测试两个版本,输入相同的100000个元素,重复100次。在我的Athlon 64 1.8 GHz(32位模式)上,第一个功能需要0.231秒,第二个(更长)功能需要0.185秒。两个函数在相同的源文件中相邻,因此不可能有不同的编译器设置。我已经多次运行测试,扭转它们运行的顺序,每次的时间大致相同。
我知道现代处理器中有很多谜,但这怎么可能?
此处用于比较Microsoft VC ++ 6编译器的相关汇编器输出。
; 173 : for (i = 0; i < iCount; ++i)
$L4455:
; 174 : {
; 175 : iScaled = ftol_ambient(*pSource * PRECISION3);
fld DWORD PTR [esi]
fmul DWORD PTR __real@4@400b8000000000000000
fstp QWORD PTR $T5011[ebp]
; 170 : int i;
; 171 : int iScaled;
; 172 : unsigned int iSRGB;
fld QWORD PTR $T5011[ebp]
; 173 : for (i = 0; i < iCount; ++i)
fistp DWORD PTR _i$5009[ebp]
; 176 : if (iScaled <= 0)
mov edx, DWORD PTR _i$5009[ebp]
test edx, edx
jg SHORT $L4458
; 177 : *pDestination = 0;
mov BYTE PTR [ecx], 0
; 178 : else if (iScaled >= PRECISION3)
jmp SHORT $L4461
$L4458:
cmp edx, 4096 ; 00001000H
jl SHORT $L4460
; 179 : *pDestination = 255;
mov BYTE PTR [ecx], 255 ; 000000ffH
; 180 : else
jmp SHORT $L4461
$L4460:
; 181 : {
; 182 : iSRGB = FloatToSRGBTable3[iScaled];
; 183 : *pDestination = (unsigned char) iSRGB;
mov dl, BYTE PTR _FloatToSRGBTable3[edx]
mov BYTE PTR [ecx], dl
$L4461:
; 184 : }
; 185 : pSource++;
add esi, 4
; 186 : pDestination++;
inc ecx
dec edi
jne SHORT $L4455
$L4472:
; 199 : {
; 200 : iScaled = ftol_ambient(*pSource * PRECISION3);
fld DWORD PTR [esi]
fmul DWORD PTR __real@4@400b8000000000000000
fstp QWORD PTR $T4865[ebp]
; 195 : int i;
; 196 : int iScaled;
; 197 : unsigned int iSRGB;
fld QWORD PTR $T4865[ebp]
; 198 : for (i = 0; i < iCount; ++i)
fistp DWORD PTR _i$4863[ebp]
; 201 : if (iScaled <= 0)
mov edx, DWORD PTR _i$4863[ebp]
test edx, edx
jg SHORT $L4475
; 202 : *pDestination = 0;
mov BYTE PTR [edi], 0
; 203 : else if (iScaled >= PRECISION3)
jmp SHORT $L4478
$L4475:
cmp edx, 4096 ; 00001000H
jl SHORT $L4477
; 204 : *pDestination = 255;
mov BYTE PTR [edi], 255 ; 000000ffH
; 205 : else
jmp SHORT $L4478
$L4477:
; 206 : {
; 207 : iSRGB = FloatToSRGBTable3[iScaled];
xor ecx, ecx
mov cl, BYTE PTR _FloatToSRGBTable3[edx]
; 208 : if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))
mov edx, DWORD PTR _SRGBCeiling[ecx*4]
cmp edx, DWORD PTR [esi]
jg SHORT $L4481
; 209 : ++iSRGB;
inc ecx
$L4481:
; 210 : *pDestination = (unsigned char) iSRGB;
mov BYTE PTR [edi], cl
$L4478:
; 211 : }
; 212 : pSource++;
add esi, 4
; 213 : pDestination++;
inc edi
dec eax
jne SHORT $L4472
<小时/> 编辑:尝试测试Nils Pipenbrinck's hypothesis,我在第一个函数的循环之前和之内添加了几行:
int one = 1;
int two = 2;
if (one == two)
++iSRGB;
第一个功能的运行时间现在下降到0.152秒。有趣。
<小时/> 编辑2: Nils指出比较将在发布版本中进行优化,实际上是。汇编代码中的更改非常微妙,我将在此处发布,以查看它是否提供了任何线索。在这一点上,我想知道它是否是代码对齐?
; 175 : for (i = 0; i < iCount; ++i)
$L4457:
; 176 : {
; 177 : iScaled = ftol_ambient(*pSource * PRECISION3);
fld DWORD PTR [edi]
fmul DWORD PTR __real@4@400b8000000000000000
fstp QWORD PTR $T5014[ebp]
; 170 : int i;
; 171 : int iScaled;
; 172 : int one = 1;
fld QWORD PTR $T5014[ebp]
; 173 : int two = 2;
fistp DWORD PTR _i$5012[ebp]
; 178 : if (iScaled <= 0)
mov esi, DWORD PTR _i$5012[ebp]
test esi, esi
jg SHORT $L4460
; 179 : *pDestination = 0;
mov BYTE PTR [edx], 0
; 180 : else if (iScaled >= PRECISION3)
jmp SHORT $L4463
$L4460:
cmp esi, 4096 ; 00001000H
jl SHORT $L4462
; 181 : *pDestination = 255;
mov BYTE PTR [edx], 255 ; 000000ffH
; 182 : else
jmp SHORT $L4463
$L4462:
; 183 : {
; 184 : iSRGB = FloatToSRGBTable3[iScaled];
xor ecx, ecx
mov cl, BYTE PTR _FloatToSRGBTable3[esi]
; 185 : if (one == two)
; 186 : ++iSRGB;
; 187 : *pDestination = (unsigned char) iSRGB;
mov BYTE PTR [edx], cl
$L4463:
; 188 : }
; 189 : pSource++;
add edi, 4
; 190 : pDestination++;
inc edx
dec eax
jne SHORT $L4457
答案 0 :(得分:11)
我的猜测是,在第一种情况下,两个不同的分支最终在CPU的同一分支预测槽中。如果这两个分支在每次代码减速时预测不同。
在第二个循环中,添加的代码可能足以将其中一个分支移动到不同的分支预测槽。
确保您可以尝试使用英特尔VTune分析器或AMD CodeAnalyst工具。这些工具将向您显示代码中的确切内容。
但是,请记住,进一步优化此代码很可能不值得。如果您在CPU上调整代码速度更快,则可能会在不同品牌上变慢。
修改强>
如果您想阅读分支预测,请尝试给Agner Fog优秀的网站:http://www.agner.org/optimize/
本pdf详细解释了分支预测时隙分配:http://www.agner.org/optimize/microarchitecture.pdf
答案 1 :(得分:4)
我的第一个猜测是在第二种情况下分支被预测得更好。可能是因为嵌套if提供了处理器使用更多信息来猜测的任何算法。出于好奇,当您删除行
时会发生什么 if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))
答案 2 :(得分:1)
你如何计算这些例程?我想知道分页或缓存是否对时间有影响?调用第一个例程可能会加载到内存中,跨越页面边界或导致堆栈进入无效页面(导致页面调入),但只有第一个例程才会付出代价。
在进行测量以减少虚拟内存和缓存可能产生的影响的调用之前,您可能希望运行两次这两个函数。
答案 3 :(得分:0)
您只是测试这个内循环,还是您正在测试未公开的外循环?如果是这样,请看这三行:
if (((int *)SRGBCeiling)[iSRGB] <= *((int *)pSource))
++iSRGB;
*pDestination = (unsigned char) iSRGB;
现在, *pDestination
似乎是外循环的计数器。因此,通过有时对 iSRGB
值进行额外增量,您可以跳过外部循环中的一些迭代,从而减少代码需要完成的工作量。
答案 4 :(得分:0)
我曾经有过类似的情况。我从循环中提取了一些代码以使其更快,但速度变慢了。混乱。事实证明,循环的平均次数小于1。
这一课(显然你不需要)是改变不会使你的代码更快,除非你测量它实际运行得更快。