我有两个版本在int数组中搜索特定值。
第一个版本是直接版本
int FindWithoutBlock(int * Arr, int ArrLen, int Val)
{
for ( int i = 0; i < ArrLen; i++ )
if ( Arr[i] == Val )
return i;
return ArrLen;
}
第二个版本应该更快。传递的数组需要比前一种情况大一个元素。假设有5个值的数组,则分配6个整数然后执行以下操作
int FindWithBlock(int * Arr, int LastCellIndex, int Val)
{
Arr[LastCellIndex] = Val;
int i;
for ( i = 0 ; Arr[i] != Val; i++ );
return i;
}
这个版本应该更快 - 你不需要通过Arr每次迭代检查数组边界。
现在是“问题”。在Debug中的100K元素阵列上运行这些函数100K次时,第二个版本大约快2倍。然而,在Release中,第一个版本的速度提高了大约6000倍。问题是为什么。
可以在http://eubie.sweb.cz/main.cpp
找到演示此功能的程序非常感谢任何见解。 丹尼尔
答案 0 :(得分:8)
以下是使用DevStudio 2005的结果:
调试:
推出:
从命令行运行它非常重要,而不是从DevStudio中运行,DevStudio会影响应用程序的性能。
了解真正发生的事情的唯一方法是查看汇编代码。这是在发布中生成的汇编程序: -
FindWithoutBlock:
00401000 xor eax,eax
00401002 cmp dword ptr [ecx+eax*4],0F4240h
00401009 je FindWithoutBlock+1Ah (40101Ah)
0040100B add eax,1
0040100E cmp eax,186A0h
00401013 jl FindWithoutBlock+2 (401002h)
00401015 mov eax,186A0h
0040101A ret
请注意,编译器已删除ArrLen参数并将其替换为常量!它也将它作为一种功能。
这是编译器对其他函数(FindWithBlock)所做的事情: -
004010E0 mov dword ptr [esp+38h],186A0h
004010E8 mov ebx,0F4240h
004010ED mov dword ptr [esi+61A80h],ebx
004010F3 xor eax,eax
004010F5 cmp dword ptr [esi],ebx
004010F7 je main+0EFh (40110Fh)
004010F9 lea esp,[esp]
00401100 add eax,1
00401103 cmp dword ptr [esi+eax*4],ebx
00401106 jne main+0E0h (401100h)
00401108 cmp eax,186A0h
0040110D je main+0F5h (401115h)
0040110F call dword ptr [__imp__getchar (4020D0h)]
00401115 sub dword ptr [esp+38h],1
0040111A jne main+0CDh (4010EDh)
这里,该功能已经内联。 lea esp,[esp]
只是一个7字节的nop来对齐下一条指令。代码分别检查索引0到所有其他索引,但主循环肯定比FindWithoutBlock版本更紧。
嗯。这是调用FindWithoutBlock的代码: -
0040106F mov ecx,edi
00401071 mov ebx,eax
00401073 call FindWithoutBlock (401000h)
00401078 mov ebp,eax
0040107A mov edi,186A0h
0040107F cmp ebp,186A0h
00401085 je main+6Dh (40108Dh)
00401087 call dword ptr [__imp__getchar (4020D0h)]
0040108D sub edi,1
00401090 jne main+5Fh (40107Fh)
啊哈! FindWitoutBlock函数只被调用一次!编译器发现该函数每次都返回相同的值,并将其优化为单个调用。在FindWithBlock中,编译器不能做出相同的假设,因为您在搜索之前写入数组,因此每个调用(可能)数组都不同。
要对此进行测试,请添加volatile
关键字,如下所示: -
int FindWithoutBlock(volatile int * Arr, int ArrLen, int Val)
{
for ( int i = 0; i < ArrLen; i++ )
if ( Arr[i] == Val )
return i;
return ArrLen;
}
int FindWithBlock(volatile int * Arr, int LastCellIndex, int Val)
{
Arr[LastCellIndex] = Val;
int i;
for ( i = 0 ; Arr[i] != Val; i++ );
return i;
}
这样做,两个版本都在相似的时间内运行(6.040)。看到内存访问是一个主要的瓶颈,FindWithoutBlock的更复杂的测试不会影响整体速度。
答案 1 :(得分:2)
std::find
和迭代器?
但其次,编译器的优化器是为了识别第一种形式而不是第二种形式。例如,它可以是内联的,展开的或矢量化的,而第二个不能是。
在一般情况下,请考虑缓存问题。您正在触摸阵列的末尾,然后转到开头 - 这可能是缓存未命中。然而,在第一个块中,您只能顺序通过数组 - 更多缓存一致。
答案 2 :(得分:2)
这更像是一个扩展的评论而不是一个答案。 Skizz已经用“ Aha回答了问题!FindWithoutBlock函数只被调用一次!”
测试驱动程序
我通常倾向于将测试驱动程序和测试文章的代码放在单独的文件中。首先,您不打算提供测试驱动程序。另一方面,像你一样组合它们让优化器做你真正不想做的事情,例如调用函数一次而不是100,000次。将它们分开可让您为驱动程序和测试文章使用不同的优化级别。我倾向于编译未经优化的驱动程序,以便执行相同的事情100K次的循环真正执行100K次。另一方面,测试文章使用预期的优化进行编译。
使用getchar()
在测试CPU利用率时,在测试循环中使用任何I / O通常是个坏主意。 当要找到的项目不在数组中时,您的测试代码正在调用getchar。 [其余的错误分析已被删除。] 更新:您的测试代码调用{{1}当要找到的项目在数组中时。即使您的测试代码确保找不到该项目(因此也不会调用getchar
),但仍然不是一个好主意。做一些快速而温和的事情。
C与C ++
您的代码看起来更像C +而不是C ++。您正在使用getchar
而不是malloc
,您正在混合使用C和C ++ I / O,并且您没有使用诸如new
之类的C ++库。对于从C转向C ++的人来说,这是典型的。很了解像std::find
这样的事情。这样您就可以完全取消std::find
功能。
过早优化
使用FindWithoutBlock
公式的唯一原因是因为这种搜索是一个瓶颈。这真的是一个瓶颈吗? FindWithBlock
公式(甚至更好,FindWithoutBlock
)可以说是更好的方法,因为您不需要修改数组,因此数组参数可以标记为std::find
。使用const
无法将数组标记为此类,因为您正在修改数组。
答案 3 :(得分:0)
我观察到的是,在第一种情况下,编译器在运行时知道循环的大小(例如&lt; ArrLen)。在第二种情况下,编译器无法知道。
答案 4 :(得分:0)
在第一个示例中,每次迭代都会检查两个条件:i < ArrLen
和Arr[i] == Val
。在第二个例子中,只有一个条件需要检查。这就是为什么第一个循环慢了两倍。
我无法使用GCC观察到相同的行为:第一个循环仍然较慢。
使用-O0
:
Without block: 25.83
With block: 20.35
使用-O3
:
Without block: 6.33
With block: 4.75
我猜编译器在某种程度上推断出数组中没有SearchVal
,因此没有理由调用搜索它的函数。
答案 5 :(得分:0)
第一个for ..循环包含每个迭代的两个条件,而第二个for循环包含每个循环一次迭代。对于大量迭代,应该显示此差异,因为第二个条件和迭代器增量之间存在RAW依赖关系。但我仍然认为加速不应该那么高。
答案 6 :(得分:0)
你的编译器很聪明。
如果您使用LLVM Try Out页面,则会获得以下红外线:
define i32 @FindWithoutBlock(i32* nocapture %Arr, i32 %ArrLen, i32 %Val) nounwind uwtable readonly
define i32 @FindWithBlock(i32* nocapture %Arr, i32 %ArrLen, i32 %Val) nounwind uwtable
唯一的区别是第一个函数上存在readonly
属性:
来自Language Reference页面:
<强>只读强>
此属性指示函数不通过任何指针参数(包括byval参数)写入或以其他方式修改调用函数可见的任何状态(例如,内存,控制寄存器等)。它可以取消引用可以在调用者中设置的指针参数和读取状态。当使用相同的参数集和全局状态调用时,只读函数始终返回相同的值(或相同地展开异常)。它不能通过调用C ++异常抛出方法来解除异常。
这意味着,优化器可能会意识到函数将始终返回相同的计算(对于给定的循环)并将其提升到循环外。