English 中文(简体)
用C交换值的最快方法是什么?
原标题:
  • 时间:2008-08-31 15:12:35
  •  标签:

I want to swap two integers, and I want to know which of these two implementations will be faster: The obvious way with a temp variable:

void swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

或者我相信大多数人都见过的xor版本:

void swap(int* a, int* b)
{
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

第一个寄存器似乎使用了一个额外的寄存器,但第二个寄存器执行三次加载和存储,而第一个寄存器只执行其中的两次。有人能告诉我哪个更快吗?为什么?为什么更重要。

最佳回答

如果a和b指向同一地址,则XOR方法将失败。第一个XOR将清除两个变量指向的内存地址处的所有位,因此一旦函数返回(*a==*b==0),无论初始值如何。

More info on the Wiki page: XOR swap algorithm

虽然这个问题不太可能出现,但我总是更喜欢使用保证有效的方法,而不是在意外时刻失败的聪明方法。

问题回答

数字2经常被引用为“聪明”的方法。事实上,它很可能更慢,因为它掩盖了程序员的明确目标——交换两个变量。这意味着编译器无法优化它以使用实际的汇编操作进行交换。它还假定能够对对象执行逐位异或。

坚持第1条,这是最通用、最容易理解的交换,可以很容易地进行模板化/通用化。

This wikipedia section explains the issues quite well: http://en.wikipedia.org/wiki/XOR_swap_algorithm#Reasons_for_avoidance_in_practice

在现代处理器上,对大型数组进行排序时可以使用以下方法,并且速度没有差异:

void swap (int *a, int *b)
{
  for (int i = 1 ; i ; i <<= 1)
  {
    if ((*a & i) != (*b & i))
    {
      *a ^= i;
      *b ^= i;
    }
  }
}

你的问题真正重要的部分是为什么?部分现在,回到20年前的8086天,上面的内容将是一个真正的性能杀手,但在最新的奔腾上,这将是你发布的两个版本的速度匹配。

原因完全在于内存,与CPU无关。

与内存速度相比,CPU速度有了惊人的提高。访问内存已成为应用程序性能的主要瓶颈。所有的交换算法都将花费大部分时间等待从内存中提取数据。现代操作系统最多可以有5个级别的内存:

  • Cache Level 1 - runs at the same speed as the CPU, has negligible access time, but is small
  • Cache Level 2 - runs a bit slower than L1 but is larger and has a bigger overhead to access (usually, data needs to be moved to L1 first)
  • Cache Level 3 - (not always present) Often external to the CPU, slower and bigger than L2
  • RAM - the main system memory, usually implements a pipeline so there s latency in read requests (CPU requests data, message sent to RAM, RAM gets data, RAM sends data to CPU)
  • Hard Disk - when there s not enough RAM, data is paged to HD which is really slow, not really under CPU control as such.

排序算法会使内存访问变得更糟,因为它们通常以非常无序的方式访问内存,从而导致从L2、RAM或HD获取数据的低效开销。

因此,优化交换方法真的毫无意义——如果只调用几次,那么由于调用次数少,任何低效率都会被隐藏起来;如果调用多次,那么由于缓存未命中的次数(CPU需要从L2(1秒周期)、L3(10s周期)、RAM(100s周期)和HD(!)中获取数据),任何低成本都会被掩盖起来。

您真正需要做的是查看调用交换方法的算法。这不是一个微不足道的练习。尽管Big-O表示法很有用,但对于小n,O(n)可能比O(logn)快得多。因此,您需要分析您的算法及其使用的数据。

这就引出了如何分析代码。探查器很有用,但您确实需要知道如何解释结果。永远不要使用一次运行来收集结果,总是在多次执行中平均结果——因为测试应用程序可能在执行到一半时被操作系统分页到硬盘上。总是评测发布、优化的构建、评测调试代码是毫无意义的。

至于最初的问题-哪个更快?-这就像试图通过观察翼镜的大小和形状来判断法拉利是否比兰博基尼更快。

第一个更快,因为像xor这样的按位操作通常很难对读者进行可视化。

当然,理解得更快,这是最重要的部分;)

Regarding @Harry: Never implement functions as macros for the following reasons:

  1. 类型安全。没有。以下仅在编译时生成警告,但在运行时失败:

    float a=1.5f,b=4.2f;
    swap (a,b);
    

    模板化函数的类型总是正确的(为什么不将警告视为错误呢?)。

    编辑:由于C中没有模板,您需要为每种类型编写一个单独的交换,或者使用一些技巧性的内存访问。

  2. 这是一个文本替换。以下操作在运行时失败(这次没有编译器警告):

    int a=1,temp=3;
    swap (a,temp);
    
  3. 这不是一个函数。因此,它不能用作qsort之类的参数。

  4. Compilers are clever. I mean really clever. Made by really clever people. They can do inlining of functions. Even at link time (which is even more clever). Don t forget that inlining increases code size. Big code means more chance of cache miss when fetching instructions, which means slower code.
  5. 副作用。宏有副作用!考虑:

    int &f1 ();
    int &f2 ();
    void func ()
    {
      swap (f1 (), f2 ());
    }
    

    在这里,f1和f2将被调用两次。

    编辑:C版本,有令人讨厌的副作用:

    int a[10], b[10], i=0, j=0;
    swap (a[i++], b[j++]);
    

宏:说不

编辑:这就是为什么我更喜欢用大写定义宏名称,以便它们在代码中脱颖而出,作为谨慎使用的警告。

编辑2:回答Leahn Novash的评论:

假设我们有一个非内联函数f,它被编译器转换为一个字节序列,那么我们可以定义字节数,如下所示:

bytes = C(p) + C(f)

其中,C()给出生成的字节数,C(f)是函数的字节,C(p)是内务代码的字节,编译器添加到函数的前导码和后amble(创建和销毁函数的堆栈帧,依此类推)。现在,调用函数f需要C(C)个字节。如果函数被调用n次,则总代码大小为:

size = C(p) + C(f) + n.C(c)

现在让我们内联这个函数。函数的内务处理C(p)变为零,因为函数可以使用调用方的堆栈帧。C(C)也是零,因为现在没有调用操作码。但是,只要有调用,f就会被复制。因此,现在的总代码大小是:

size = n.C(f)

现在,如果C(f)小于C(C),那么整个可执行文件的大小将减小。但是,如果C(f)大于C(C),那么代码大小将增加。如果C(f)和C(C)相似,那么您也需要考虑C(p)。

那么,C(f)和C(C)产生了多少字节。好吧,最简单的C++函数就是getter:

void GetValue () { return m_value; }

这可能会生成四字节指令:

mov eax,[ecx + offsetof (m_value)]

这是四个字节。调用指令是五个字节。因此,可以节省整体尺寸。如果函数更复杂,比如索引器(“return m_value[index];”)或计算(“return m_value_a+m_value_b;”),那么代码会更大。

对于那些偶然发现这个问题并决定使用XOR方法的人来说。您应该考虑内联函数或使用宏来避免函数调用的开销:

#define swap(a, b)   
do {                 
    int temp = a;    
    a = b;           
    b = temp;        
} while(0)

从未理解对宏的仇恨。如果使用得当,它们可以使代码更加紧凑和可读。我相信大多数程序员都知道应该谨慎使用宏,重要的是要明确一个特定的调用是宏,而不是函数调用(全部大写)。如果<code>SWAP(a++,b++)是问题的一贯来源,也许编程不适合您。

诚然,xor技巧在你看到它的前5000次时是很巧妙的,但它真正做的只是以牺牲可靠性为代价临时保存一个。查看上面生成的程序集,它保存了一个寄存器,但创建了依赖项。此外,我不建议使用xchg,因为它有一个隐含的锁前缀。

最终,我们都来到了同一个地方,因为我们最聪明的代码导致了无数个小时浪费在无效的优化和调试上——保持简单。

#define SWAP(type, a, b) 
    do { type t=(a);(a)=(b);(b)=t; } while (0)

void swap(size_t esize, void* a, void* b)
{
    char* x = (char*) a;
    char* y = (char*) b;
    char* z = x + esize;

    for ( ; x < z; x++, y++ )
        SWAP(char, *x, *y);
}

你在优化错误的东西,这两种方法都应该很快,以至于你必须运行它们数十亿次才能获得任何可测量的差异。

几乎任何事情都会对您的性能产生更大的影响,例如,如果您正在交换的值在内存中与您触摸的最后一个值接近,那么它们很可能在处理器缓存中,否则您将不得不访问内存,这比您在处理器内执行的任何操作都慢几个数量级。

无论如何,你的瓶颈更可能是低效的算法或不适当的数据结构(或通信开销),而不是你如何交换号码。

真正知道的唯一方法是测试它,答案甚至可能因您所在的编译器和平台而异。如今,现代编译器非常擅长优化代码,除非您能证明自己的方法真的更快,否则永远不要试图智胜编译器。

话虽如此,你最好有充分的理由选择#2而不是#1。#1中的代码可读性要高得多,因此应该始终首先选择。只有当你能证明你需要来做出改变时,才切换到#2,如果你这样做了,请用非显而易见的方式评论它,解释发生了什么以及你为什么这么做。

作为一则轶事,我与喜欢的几个人合作,过早地进行优化,这会产生非常可怕、无法维护的代码。我还敢打赌,他们经常会自食其果,因为他们以非直接的方式编写代码,削弱了编译器优化代码的能力。

对于现代CPU体系结构,方法1将比方法2更快,可读性也更高。

在现代CPU体系结构上,XOR技术比使用临时变量进行交换要慢得多。其中一个原因是现代CPU努力通过指令管道并行执行指令。在XOR技术中,每个操作的输入取决于前一个操作的结果,因此它们必须严格按顺序执行。如果效率非常重要,建议在目标体系结构上测试XOR技术和临时变量交换的速度。查看此处了解更多信息。


编辑:方法2是就地交换的一种方式(即不使用额外的变量)。为了完成这个问题,我将使用+/-添加另一个就地交换。

void swap(int* a, int* b)
{
    if (a != b) // important to handle a/b share the same reference
    {
        *a = *a+*b;
        *b = *a-*b;
        *a = *a-*b;
    }
}

除非迫不得已,否则我不会使用指针。编译器无法很好地优化它们,因为指针别名(尽管如果可以保证指针指向非重叠位置,GCC至少有扩展来优化这一点)。

我根本不会用函数来做,因为这是一个非常简单的操作,函数调用开销很大。

如果你需要原始速度和优化的可能性,那么最好的方法就是使用宏。在GCC中,您可以使用typeof()内建函数来制作一个适用于任何内置类型的灵活版本。

类似这样的内容:

#define swap(a,b) 
  do { 
    typeof(a) temp; 
    temp = a; 
    a = b; 
    b = temp; 
  } while (0)

...    
{
  int a, b;
  swap(a, b);
  unsigned char x, y;
  swap(x, y);                 /* works with any type */
}

对于其他编译器,或者如果您需要严格遵守标准C89/99,则必须为每种类型制作一个单独的宏。

如果使用局部/全局变量作为参数进行调用,一个好的编译器将在给定上下文的情况下尽可能积极地对此进行优化。

所有评分最高的答案实际上并不是决定性的“事实”。。。他们是在投机的人!

您可以明确地知道哪些代码执行的汇编指令更少,因为您可以查看编译器生成的输出汇编,并查看哪些代码执行汇编指令更少!

这是我用标志“gcc-std=c99-S-O3 lookingAtAsmOutput.c”编译的c代码:

#include <stdio.h>
#include <stdlib.h>

void swap_traditional(int * restrict a, int * restrict b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

void swap_xor(int * restrict a, int * restrict b)
{
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

int main() {
    int a = 5;
    int b = 6;
    swap_traditional(&a,&b);
    swap_xor(&a,&b);
}

swap_traditional()的ASM输出占用>>>;11<<&书信电报;说明(不包括“离开”、“返回”、“大小”):

.globl swap_traditional
    .type   swap_traditional, @function
swap_traditional:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %edx
    movl    12(%ebp), %ecx
    pushl   %ebx
    movl    (%edx), %ebx
    movl    (%ecx), %eax
    movl    %ebx, (%ecx)
    movl    %eax, (%edx)
    popl    %ebx
    popl    %ebp
    ret
    .size   swap_traditional, .-swap_traditional
    .p2align 4,,15

swap_xor()的ASM输出占用>>>;11<<&书信电报;不包括“离开”和“返回”的说明:

.globl swap_xor
    .type   swap_xor, @function
swap_xor:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %ecx
    movl    12(%ebp), %edx
    movl    (%ecx), %eax
    xorl    (%edx), %eax
    movl    %eax, (%ecx)
    xorl    (%edx), %eax
    xorl    %eax, (%ecx)
    movl    %eax, (%edx)
    popl    %ebp
    ret
    .size   swap_xor, .-swap_xor
    .p2align 4,,15

Summary of assembly output:
swap_traditional() takes 11 instructions
swap_xor() takes 11 instructions

Conclusion:
Both methods use the same amount of instructions to execute and therefore are approximately the same speed on this hardware platform.

Lesson learned:
When you have small code snippets, looking at the asm output is helpful to rapidly iterate your code and come up with the fastest ( i.e. least instructions ) code. And you can save time even because you don t have to run the program for each code change. You only need to run the code change at the end with a profiler to show that your code changes are faster.

对于需要速度的繁重DSP代码,我经常使用这种方法。

要回答您的问题,需要深入研究该代码将在其上运行的特定CPU的指令时序,因此需要我围绕系统中缓存的状态和编译器发出的汇编代码做出一系列假设。从理解您选择的处理器实际工作方式的角度来看,这将是一个有趣而有用的练习,但在现实世界中,差异将微不足道。

x=x+y-(y=x);

float x; cout << "X:"; cin >> x;
float y; cout << "Y:" ; cin >> y;

cout << "---------------------" << endl;
cout << "X=" << x << ", Y=" << y << endl;
x=x+y-(y=x);
cout << "X=" << x << ", Y=" << y << endl;

在我看来,像这样的局部优化应该只被认为与平台密切相关。如果你在16位uC编译器上编译,或者在以x64为目标的gcc上编译,这将产生巨大的不同。

如果你心中有一个特定的目标,那么只需尝试这两种方法,看看生成的asm代码,或者用这两种方式评测你的应用程序,看看哪一种在你的平台上实际上更快。

如果您可以使用一些内联汇编程序并执行以下操作(psuedo汇编程序):

PUSH A
A=B
POP B

您将保存大量的参数传递和堆栈修复代码等。

我刚刚把这两个交换(作为宏)放在我一直在玩的手写快速排序中。XOR版本比带有临时变量的版本(0.6秒)快得多(0.1秒)。然而,XOR确实损坏了数组中的数据(可能与Ant提到的地址相同)。

As it was a fat pivot quicksort, the XOR version s speed is probably from making large portions of the array the same. I tried a third version of swap which was the easiest to understand and it had the same time as the single temporary version.


acopy=a;
bcopy=b;
a=bcopy;
b=acopy;

[I just put an if statements around each swap, so it won t try to swap with itself, and the XOR now takes the same time as the others (0.6 sec)]

如果您的编译器支持内联汇编程序,并且您的目标是32位x86,那么XCHG指令可能是实现这一点的最佳方法。。。如果你真的那么在乎表现的话。

以下是一种与MSVC++一起工作的方法:

#include <stdio.h>

#define exchange(a,b)   __asm mov eax, a 
                        __asm xchg eax, b 
                        __asm mov a, eax               

int main(int arg, char** argv)
{
    int a = 1, b = 2;
    printf("%d %d --> ", a, b);
    exchange(a,b)
    printf("%d %d
", a, b);
    return 0;
}




相关问题
热门标签