我有以下功能
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 ticks和1.99seconds 使用常数:330000刻度和0.33秒
编辑: 使用带有-03编译的argumenst:http://pastebin.com/fW2HCnHc 使用常量-03编译:http://pastebin.com/FHs68Agi
答案 0 :(得分: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
的值是在某个循环中生成的。我会把这个循环推到这个函数中。如果编译器知道值是作为循环的一部分生成的,那么就会有大量的优化选项。在极端情况下,它可以使用一些SIMD指令,这些指令是各种SSE或3dnow的一部分!指令套件使得事情比使用常量的版本快得多。
另一个选项是使这个函数内联,这将暗示编译器应该尝试将它插入到调用它的循环中。如果编译器接受了提示(这个函数有点大,那么编译器可能没有),它会产生与将循环填充到函数中相同的效果。
答案 1 :(得分:2)
嗯,immediate constant vs. memory
格式的二进制算术运算可以产生比memory vs. memory
格式更快的代码,但是你观察到的时间效应似乎过于极端,特别是考虑到还有其他该功能内部的操作。
可能是编译器决定内联你的函数吗?内联将允许编译器轻松地消除与第二个版本中未使用的patch_top_left_row
和patch_top_left_col
参数相关的所有内容,包括在调用代码中准备/计算这些参数的任何步骤。
从技术上讲,即使函数没有内联也可以这样做,但通常会更复杂。