性能问题C ++ - 通过数组搜索

时间:2012-05-25 10:24:16

标签: c++ performance

我有两个版本在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

找到演示此功能的程序

非常感谢任何见解。 丹尼尔

7 个答案:

答案 0 :(得分:8)

以下是使用DevStudio 2005的结果:

调试:

  • 没有阻止:25.109
  • 使用块:19.703

推出:

  • 没有阻止:0
  • 使用块:6.046

从命令行运行它非常重要,而不是从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)

首先,ewwwwww令人厌恶的C垃圾。 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 < ArrLenArr[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 ++异常抛出方法来解除异常。

这意味着,优化器可能会意识到函数将始终返回相同的计算(对于给定的循环)并将其提升到循环外。