我在InterviewBit中尝试了关于数组的问题。在这个问题中,我创建了一个内联函数,返回整数的绝对值。但有人告诉我,我的算法在提交时效率不高。但当我改为使用C ++库中的abs()
时,它给出了一个正确的答案判决。
这是我的函数得到效率低下的判决 -
inline int abs(int x){return x>0 ? x : -x;}
int Solution::coverPoints(vector<int> &X, vector<int> &Y) {
int l = X.size();
int i = 0;
int ans = 0;
while (i<l-1){
ans = ans + max(abs(X[i]-X[i+1]), abs(Y[i]-Y[i+1]));
i++;
}
return ans;
}
这是获得正确答案的那个 -
int Solution::coverPoints(vector<int> &X, vector<int> &Y) {
int l = X.size();
int i = 0;
int ans = 0;
while (i<l-1){
ans = ans + max(abs(X[i]-X[i+1]), abs(Y[i]-Y[i+1]));
i++;
}
return ans;
}
为什么会发生这种情况,因为我认为内联函数最快,因为没有调用?或者该网站有错误?如果网站正确,那么C ++ abs()
使用哪个比inline abs()
更快?
答案 0 :(得分:25)
我不同意他们的判决。他们显然错误。
在当前的优化编译器上,两种解决方案都会产生完全相同的输出。甚至,如果他们没有产生完全相同,他们将产生与库一样高效的代码(一切都匹配可能有点令人惊讶:算法,使用的寄存器。也许是因为实际的库实现与OP的实现相同?)。
没有理智的优化编译器会在你的abs()
代码中创建分支(如果可以在没有分支的情况下完成),正如其他答案所暗示的那样。如果编译器没有进行优化,那么它可能不会内联库abs()
,因此它也不会很快。
优化abs()
是编译器最容易做的事情之一(只需在窥孔优化器中为它添加一个条目,并完成)。
此外,我过去曾见过库实现,其中abs()
被实现为非内联库函数(虽然很久以前)。
证明两种实现方式相同:
GCC
myabs:
mov edx, edi ; argument passed in EDI by System V AMD64 calling convention
mov eax, edi
sar edx, 31
xor eax, edx
sub eax, edx
ret
libabs:
mov edx, edi ; argument passed in EDI by System V AMD64 calling convention
mov eax, edi
sar edx, 31
xor eax, edx
sub eax, edx
ret
锵:
myabs:
mov eax, edi ; argument passed in EDI by System V AMD64 calling convention
neg eax
cmovl eax, edi
ret
libabs:
mov eax, edi ; argument passed in EDI by System V AMD64 calling convention
neg eax
cmovl eax, edi
ret
Visual Studio(MSVC):
libabs:
mov eax, ecx ; argument passed in ECX by Windows 64-bit calling convention
cdq
xor eax, edx
sub eax, edx
ret 0
myabs:
mov eax, ecx ; argument passed in ECX by Windows 64-bit calling convention
cdq
xor eax, edx
sub eax, edx
ret 0
ICC:
myabs:
mov eax, edi ; argument passed in EDI by System V AMD64 calling convention
cdq
xor edi, edx
sub edi, edx
mov eax, edi
ret
libabs:
mov eax, edi ; argument passed in EDI by System V AMD64 calling convention
cdq
xor edi, edx
sub edi, edx
mov eax, edi
ret
在Godbolt Compiler Explorer上的See for yourself,您可以在其中检查各种编译器生成的机器代码。 (链接由Peter Cordes友情提供。)
答案 1 :(得分:19)
您的abs
根据条件执行分支。虽然内置变体只是从整数中删除了符号位,但很可能只使用了几条指令。可能的装配示例(取自here):
cdq
xor eax, edx
sub eax, edx
cdq复制寄存器eax的符号以注册edx。例如,如果它是正数,则edx将为零,否则,edx将为0xFFFFFF,表示-1。如果是正数,则带有原点编号的xor操作将不会改变任何数字(任何数字x或0都不会改变)。但是,当eax为负时,eax xor 0xFFFFFF会产生(不是eax)。最后一步是从eax中减去edx。同样,如果eax为正,则edx为零,并且最终值仍然相同。对于负值,(~eax) - (-1)= -eax这是所需的值。
正如您所看到的,这种方法只使用三个简单的算术指令而根本没有条件分支。
编辑:经过一番研究后发现,abs的许多内置实现使用相同的方法return __x >= 0 ? __x : -__x;
,这样的模式是编译器优化避免的明显目标不必要的分支。
但是,这并不能证明使用自定义abs
实施是合理的,因为它违反了DRY原则,没有人可以保证您的实施对于更复杂的方案和/或不寻常的平台。通常,只有在存在明确的性能问题或在现有实现中检测到某些其他缺陷时,才应考虑重写某些库函数。
Edit2 :只需从int切换到float即可显着降低性能:
float libfoo(float x)
{
return ::std::fabs(x);
}
andps xmm0, xmmword ptr [rip + .LCPI0_0]
自定义版本:
inline float my_fabs(float x)
{
return x>0.0f?x:-x;
}
float myfoo(float x)
{
return my_fabs(x);
}
movaps xmm1, xmmword ptr [rip + .LCPI1_0] # xmm1 = [-0.000000e+00,-0.000000e+00,-0.000000e+00,-0.000000e+00]
xorps xmm1, xmm0
xorps xmm2, xmm2
cmpltss xmm2, xmm0
andps xmm0, xmm2
andnps xmm2, xmm1
orps xmm0, xmm2
答案 2 :(得分:8)
如果您使用标准库版本,您的解决方案可能会被教科书“更清晰”,但我认为评估是错误的。您的代码被拒绝的确没有正当理由。
这是其中一个人正式正确(通过教科书)的情况之一,但坚持以纯粹的愚蠢方式知道唯一正确的解决方案,而不是接受替代解决方案并说“......但这是最好的做法,你知道”。
从技术上讲,这是一种正确,实用的方法,可以说“使用标准库,这就是它的用途,并且可能会尽可能地优化”。即使“尽可能多地优化”部分,但在某些情况下,由于标准对某些算法和/或容器施加的某些限制,很可能会出错。
现在,除了意见,最佳实践和宗教。事实上,如果你比较两种方法......
int main(int argc, char**)
{
40f360: 53 push %rbx
40f361: 48 83 ec 20 sub $0x20,%rsp
40f365: 89 cb mov %ecx,%ebx
40f367: e8 a4 be ff ff callq 40b210 <__main>
return std::abs(argc);
40f36c: 89 da mov %ebx,%edx
40f36e: 89 d8 mov %ebx,%eax
40f370: c1 fa 1f sar $0x1f,%edx
40f373: 31 d0 xor %edx,%eax
40f375: 29 d0 sub %edx,%eax
//}
int main(int argc, char**)
{
40f360: 53 push %rbx
40f361: 48 83 ec 20 sub $0x20,%rsp
40f365: 89 cb mov %ecx,%ebx
40f367: e8 a4 be ff ff callq 40b210 <__main>
return (argc > 0) ? argc : -argc;
40f36c: 89 da mov %ebx,%edx
40f36e: 89 d8 mov %ebx,%eax
40f370: c1 fa 1f sar $0x1f,%edx
40f373: 31 d0 xor %edx,%eax
40f375: 29 d0 sub %edx,%eax
//}
...它们会导致完全相同的相同说明。
但即使编译器确实使用了比较后跟条件移动(它可以在更复杂的“分支赋值”中进行,并且例如在{{1}的情况下它会做}} / min
),这可能是一个CPU周期或比黑客攻击速度慢,所以除非你做了几百万次,否则“无效”的说法无论如何都是有点怀疑的。
一次缓存未命中,你有一百倍的条件移动惩罚。
支持和反对这两种方法都有正确的论据,我不会详细讨论。我的观点是,将OP的解决方案拒绝为“完全错误”因为这种小而不重要的细节是相当狭隘的。
编辑:
(有趣的琐事)
我只是试着在我使用较旧版本的GCC(5.4与上述7.1相比)的Linux Mint盒子上寻找乐趣并且没有任何利润。
由于我包括max
没有太多想法(嘿,像<cmath>
这样的函数非常清楚属于数学,不是吗!)而不是{承载整数重载的{1}},结果是...... 令人惊讶。调用库函数远低于单表达式包装器。
现在,为了保护标准库,如果你包含abs
,那么,在任何一种情况下,生成的输出都是完全相同的。
供参考,测试代码如下:
<cstdlib>
...导致
<cstdlib>
现在,显然很容易陷入无意中使用错误的标准库函数的陷阱(我演示了自己多么容易!)。所有这些都没有来自编译器的任何警告,例如“嘿,你知道,你在整数值上使用#ifdef DRY
#include <cmath>
int main(int argc, char**)
{
return std::abs(argc);
}
#else
int abs(int v) noexcept { return (v >= 0) ? v : -v; }
int main(int argc, char**)
{
return abs(argc);
}
#endif
重载(好吧,显然没有警告,这是一个有效的转换)。” p>
考虑到这一点,可能是另一个“理由”,为什么OP提供他自己的单行并不是那么可怕的坏事。毕竟,他可能犯了同样的错误。