C++:为什么这会加速我的代码?

4
我有以下函数。
double single_channel_add(int patch_top_left_row, int patch_top_left_col, 
        int image_hash_key, 
        Mat* preloaded_images,
        int* random_values){

    int first_pixel_row = patch_top_left_row + random_values[0];
    int first_pixel_col = patch_top_left_col + random_values[1];
    int second_pixel_row = patch_top_left_row + random_values[2];
    int second_pixel_col = patch_top_left_col + random_values[3];

    int channel = random_values[4];

    Vec3b* first_pixel_bgr = preloaded_images[image_hash_key].ptr<Vec3b>(first_pixel_row, first_pixel_col);
    Vec3b* second_pixel_bgr = preloaded_images[image_hash_key].ptr<Vec3b>(second_pixel_row, second_pixel_col);

    return (*first_pixel_bgr)[channel] + (*second_pixel_bgr)[channel];
}

这个程序中,patch_top_left_rowpatch_top_left_col两个变量的值不断改变,大约被调用了一百五十万次。这个程序需要大约2秒运行完毕。当我将first_pixel_row等变量从使用参数改为直接使用硬编码的数值时(如下所示),程序的运行时间快了很多,但我不知道原因。可能是编译器进行了某些优化(我在使用gcc交叉编译器)。

double single_channel_add(int patch_top_left_row, int patch_top_left_col, 
        int image_hash_key, 
        Mat* preloaded_images,
        int* random_values){

        int first_pixel_row = 5 + random_values[0];
        int first_pixel_col = 6 + random_values[1];
        int second_pixel_row = 8 + random_values[2];
        int second_pixel_col = 10 + random_values[3];
            int channel = random_values[4];

    Vec3b* first_pixel_bgr = preloaded_images[image_hash_key].ptr<Vec3b>(first_pixel_row, first_pixel_col);
    Vec3b* second_pixel_bgr = preloaded_images[image_hash_key].ptr<Vec3b>(second_pixel_row, second_pixel_col);

    return (*first_pixel_bgr)[channel] + (*second_pixel_bgr)[channel];
}

编辑:

我已经粘贴了两个版本函数的汇编代码:

使用参数:http://pastebin.com/tpCi8c0F 使用常量:http://pastebin.com/bV0d7QH7

编辑:

在使用-O3编译后,我得到了以下时钟周期和速度:

使用参数:1990000个时钟周期和1.99秒 使用常量:330000个时钟周期和0.33秒

编辑: 使用-03编译的参数:http://pastebin.com/fW2HCnHc 使用-03编译的常量:http://pastebin.com/FHs68Agi


5
gcc有命令行选项可用于查看汇编输出。为什么不获取你在这里展示的两个代码片段的汇编代码,进行比较,并将结果差异发布在你的问题中呢? - Alex D
2
你能不能按照 SSCCE 的方式重现一下这个问题呢? - leftaroundabout
2
看一下生成的汇编代码。在第二种情况下,编译器可能使用了一些SSE指令。此外,这两种实现似乎并没有做同样的事情。 - James
为什么第二个例子中的值是5、6、8和10,而不是像复制第一个例子一样应该是5、6、5、6?我并不认为这是加速的源头,但我很好奇。 - Omnifarious
我们还需要调用这个函数的代码所对应的汇编语言,因为这个函数的调用可能会被内联展开,这种情况下你刚刚展示的代码将不会被执行到... - abarnert
显示剩余20条评论
2个回答

5
在x86平台上,有一些指令可以快速地将小整数加到寄存器中。这些指令是“lea”(也称为“加载有效地址”)指令,用于计算结构体等的地址偏移量。被加的小整数实际上是指令的一部分。聪明的编译器知道这些指令非常快,即使没有涉及地址,也会使用它们进行加法运算。
我敢打赌,如果你把常量改成至少24位长的某个随机值,那么你会看到大部分速度优化消失了。
其次,这些常量是已知的值。编译器可以尽可能高效地安排这些值以便于在寄存器中使用。对于参数,除非参数是通过寄存器传递的(我认为您的函数具有太多的参数使得该调用约定无法使用),否则编译器别无选择,只能使用堆栈偏移量加载指令从内存中获取数字。虽然这不是特别慢的指令或任何东西,但在处理常量时,编译器可以自由地做一些比简单地从指令本身获取数字更快的事情。 这些`lea`指令只是其中最极端的一个例子。 编辑:现在您已经粘贴了汇编代码,事情变得更加清晰了 在非常量代码中,以下是如何进行加法运算的:
addl    -68(%rbp), %eax

这行代码从堆栈中偏移-68(%rpb)处获取一个值,并将其加到%eax%寄存器中。
在常数代码中,这里是如何执行加法的:
addl    $5, %eax

如果你看一下具体的数据,你会看到这样的情况:

0138 83C005

很明显,被添加的常量直接编码到指令中作为一个小值。这样做比从堆栈偏移处获取值要快得多,原因有很多。首先,它更小。其次,它是没有分支的指令流的一部分。因此,它将被预取和流水线处理,没有任何缓存停顿的可能性。
所以,虽然我对于lea指令的猜测不正确,但我仍然在正确的轨道上。常量版本使用了一个专门针对向寄存器添加小整数的小指令。非常量版本必须从堆栈偏移处获取可能大小不定的整数(因此必须获取所有位,而不仅仅是低位),并添加额外的加法来计算实际地址。
编辑2:现在你已经发布了-O3结果
好吧,现在更加混乱了。它显然内联了所讨论的函数,并且在调用函数的代码和内联函数的代码之间跳来跳去。我需要看到整个文件的原始代码才能进行适当的分析。
但我强烈怀疑现在从get_random_number_in_range检索的值的不可预测性严重限制了编译器可用的优化选项。实际上,看起来在常量版本中,它甚至不必调用get_random_number_in_range,因为该值被丢弃并且从未使用。
我假设patch_top_left_row和patch_top_left_col的值在某个循环中生成。我会将此循环推入此函数。如果编译器知道这些值是作为循环的一部分生成的,则有非常多的优化选项可供选择。在极端情况下,它可以使用一些SSE或3dnow!指令套件中的SIMD指令,使事情比使用常量的版本更快。
另一个选择是将此函数内联,这将提示编译器尝试将其插入调用它的循环中。如果编译器接受提示(这个函数有点大,所以编译器可能不会),它将产生与将循环塞入函数中相同的效果。

如果您查看他发布的汇编代码,编译器并没有使用带有常量偏移量的lea指令,而是明确地将常量添加到变量中,然后在mov指令中使用这些变量作为偏移量。 - abarnert
@Omnifarious 我明白了,我已经将函数内联并使用-03编译,但仍然看到很大的时间差异(我已编辑问题)。常量版本需要0.33秒,而非常量版本需要1.99秒 - 即使它从堆栈中获取值,这个时间差异也太大了吧? - Aly
@Aly:你确定你发布的汇编代码是使用“-O3”编译选项编译的结果吗? - Omnifarious
@Aly:在使用了“-O3”之后,我的分析基本上保持不变。 - Omnifarious
@Aly:实际上,我撤回之前的话。正如我所预料的那样,-O3使情况变得更加复杂了。你能发布整个文件的原始代码,而不仅仅是一个函数吗? - Omnifarious
显示剩余3条评论

2

嗯,预计使用“立即常数 vs. 存储器”格式的二进制算术运算将产生比“存储器 vs. 存储器”更快的代码,但您观察到的时间效果似乎太极端了,特别是考虑到该函数中还有其他操作。

编译器是否决定内联您的函数?内联可以使编译器轻松消除第二个版本中未使用的“patch_top_left_row”和“patch_top_left_col”参数以及调用代码中准备/计算这些参数的任何步骤。

从技术上讲,即使没有内联函数,也可以做到这一点,但通常更加复杂。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接