在我写的一个程序中,20%的时间用于在内循环中查找最少3个数字,在此例程中:
static inline unsigned int
min(unsigned int a, unsigned int b, unsigned int c)
{
unsigned int m = a;
if (m > b) m = b;
if (m > c) m = c;
return m;
}
有没有办法加快速度?对于x86 / x86_64,我也可以使用汇编代码。
编辑:回复一些评论:
*正在使用的编译器是gcc 4.3.3
*就集会而言,我只是初学者。我在这里要求组装,学习如何做到这一点。 :)
*我运行四核Intel 64,因此支持MMX / SSE等。
*这里很难发布循环,但我可以告诉你它是levenshtein算法的一个高度优化的实现。
这就是编译器为我提供的非内联版本的min:
.globl min
.type min, @function
min:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl 12(%ebp), %eax
movl 16(%ebp), %ecx
cmpl %edx, %eax
jbe .L2
movl %edx, %eax
.L2:
cmpl %ecx, %eax
jbe .L3
movl %ecx, %eax
.L3:
popl %ebp
ret
.size min, .-min
.ident "GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3"
.section .note.GNU-stack,"",@progbits
内联版本在-O2优化代码内(甚至我的标记mrk = 0xfefefefe,在调用min()之前和之后)都被gcc优化掉了,所以我无法掌握它。
更新:我测试了Nils建议的更改,但是使用min()的汇编版本没有明显的性能提升。但是,通过使用-march = i686编译程序,我获得了12.5%的提升,我想这是因为整个程序正在获得gcc使用此选项生成的新的更快指令的好处。谢谢你的帮助。
P.S。 - 我使用ruby探测器来测量性能(我的C程序是一个由ruby程序加载的共享库),所以我只能花时间花在ruby程序调用的顶级C函数上,最终调用min(在堆栈中。请参阅此question。
答案 0 :(得分:11)
假设你的编译器没有去吃午餐,这应该编译为两个比较和两个条件移动。不可能做得更好。
如果发布编译器实际生成的程序集,我们可以看到是否有任何不必要的东西会降低它的速度。
要检查的首要问题是例程实际上是内联的。编译器没有义务这样做,如果它正在生成一个函数调用,那么对于这样一个简单的操作来说这将是非常昂贵的。
如果调用真正被内联,那么循环展开可能是有益的,正如DigitalRoss所说,或者矢量化是可能的。
编辑:如果您想要对代码进行矢量化,并使用最新的x86处理器,则需要使用SSE4.1 pminud
指令(内在:_mm_min_epu32
}),它取两个四个无符号整数的向量,并产生一个四个无符号整数的向量。结果的每个元素都是两个输入中相应元素的最小值。
我还注意到你的编译器使用了分支而不是条件移动;你可能应该首先尝试一个使用条件移动的版本,看看在你进行矢量实现的比赛之前是否能让你获得任何加速。
答案 1 :(得分:11)
首先确保使用适当的-march
设置。 GCC默认不使用原始i386不支持的任何指令 - 允许它使用更新的指令集有时会产生巨大的差异!在-march=core2 -O2
我得到:
min:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %edx
movl 12(%ebp), %ecx
movl 16(%ebp), %eax
cmpl %edx, %ecx
leave
cmovbe %ecx, %edx
cmpl %eax, %edx
cmovbe %edx, %eax
ret
在这里使用cmov可以帮助您避免分支延迟 - 并且只需传入-march
就可以在没有任何内联asm的情况下获得它。当内联到更大的功能时,这可能更有效,可能只有四个装配操作。如果你需要比这更快的东西,看看你是否可以让SSE向量操作在整个算法的上下文中工作。
答案 2 :(得分:6)
我对x86汇编器实现,GCC语法的看法。翻译成另一个内联汇编语法应该是微不足道的:
int inline least (int a, int b, int c)
{
int result;
__asm__ ("mov %1, %0\n\t"
"cmp %0, %2\n\t"
"cmovle %2, %0\n\t"
"cmp %0, %3\n\t"
"cmovle %3, %0\n\t"
: "=r"(result) :
"r"(a), "r"(b), "r"(c)
);
return result;
}
新版和改进版:
int inline least (int a, int b, int c)
{
__asm__ (
"cmp %0, %1\n\t"
"cmovle %1, %0\n\t"
"cmp %0, %2\n\t"
"cmovle %2, %0\n\t"
: "+r"(a) :
"%r"(b), "r"(c)
);
return a;
}
注意:它可能也可能不比C代码快。
这取决于很多因素。如果分支不可预测,通常cmov会胜出(在某些x86架构上)OTOH内联汇编器总是优化器的问题,因此对周围代码的优化惩罚可能会超过所有增益..
Btw Sudhanshu,听听这段代码如何与你的testdata一起表现会很有趣。答案 3 :(得分:5)
SSE2指令扩展包含一个整数min
指令,一次可以选择8个最小值。请参阅http://www.intel.com/software/products/compilers/clin/docs/ug_cpp/comm1046.htm
_mm_mulhi_epu16
答案 4 :(得分:5)
我的AMD Phenom上的替换时钟速度提高了约1.5%:
static inline unsigned int
min(unsigned int a, unsigned int b, unsigned int c)
{
asm("cmp %1,%0\n"
"cmova %1,%0\n"
"cmp %2,%0\n"
"cmova %2,%0\n"
: "+r" (a) : "r" (b), "r" (c));
return a;
}
结果可能有所不同;一些x86处理器不能很好地处理CMOV。
答案 5 :(得分:1)
首先,看一下反汇编。那会告诉你很多。例如,正如所写,有2个if语句(这意味着有2个可能的分支错误预测),但我的猜测是一个体面的现代C编译器将有一些聪明的优化,可以在没有分支的情况下完成。我很想知道。
其次,如果您的libc具有特殊的内置最小/最大功能,请使用它们。例如,GNU libc的浮点数为fmin / fmax,他们声称“在某些处理器上,这些函数可以使用特殊的机器指令比同等的C代码更快地执行这些操作”。也许有类似的东西。
最后,如果你对一堆数字并行执行此操作,可能会有向量指令来执行此操作,这可以提供显着的加速。但是我甚至看到使用矢量单位时非矢量代码更快。像“将一个uint加载到向量寄存器,调用向量最小函数,得到结果”这样的东西看起来很笨,但实际上可能更快。
答案 6 :(得分:0)
如果您只进行一次比较,则可能需要手动展开循环。
首先,看看你是否可以让编译器为你展开循环,如果你不能,请自己动手。这至少会减少循环控制开销...
答案 7 :(得分:0)
您可以尝试这样的方法来保存声明和不必要的比较:
static inline unsigned int
min(unsigned int a, unsigned int b, unsigned int c)
{
if (a < b)
{
if (a < c)
return a;
else
return c;
}
if (b < c)
return b;
else return c;
}
答案 8 :(得分:0)
是的,在汇编后,但我天真的优化是:
static inline unsigned int
min(unsigned int a, unsigned int b, unsigned int c)
{
unsigned int m = a;
if (m > b) m = b;
if (m > c) return c;
return m;
}
答案 9 :(得分:0)
这些都是很好的答案。如果被指责不回答问题的风险,我也会看其他80%的时间。 Stackshots是我最喜欢找到值得优化的代码的方法,特别是如果它是您发现的并不是绝对需要的函数调用。