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_row和具有不同的值patch_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

4

2 回答 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_rowpatch_top_left_col在某个循环中生成的。我会把这个循环推到这个函数中。如果编译器知道这些值是作为循环的一部分生成的,那么就有大量的优化选项可供它使用。在极端情况下,它可以使用一些 SIMD 指令,这些指令是各种 SSE 或 3dnow 的一部分!指令套件使事情变得比使用常量的版本更快。

另一种选择是使这个函数内联,这会提示编译器它应该尝试将它插入到调用它的循环中。如果编译器接受了提示(这个函数有点大,所以编译器可能不会),它会产生与将循环填充到函数中一样的效果。

于 2012-11-02T18:40:51.063 回答
2

好吧,immediate constant vs. memory格式的二进制算术运算预计会产生比memory vs. memory格式更快的代码,但是您观察到的时间效应似乎过于极端,特别是考虑到该函数内部还有其他操作。

难道是编译器决定内联你的函数?内联将允许编译器轻松消除与第二版中未使用patch_top_left_rowpatch_top_left_col参数相关的所有内容,包括在调用代码中准备/计算这些参数的任何步骤。

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

于 2012-11-02T18:23:38.383 回答