在C中交换值的最快方法是什么?

时间:2008-08-31 15:12:36

标签: c performance

我想交换两个整数,我想知道这两个实现中的哪一个会更快: 使用临时变量的显而易见的方法:

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;
}

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

21 个答案:

答案 0 :(得分:94)

经常引用2号作为“聪明”的方式。实际上它很可能更慢,因为它模糊了程序员的明确目标 - 交换两个变量。这意味着编译器无法优化它以使用实际的汇编程序操作来交换。它还假设能够对对象执行按位xor。

坚持1号,它是最通用和最易理解的交换,可以很容易地模板化/通用化。

这个维基百科部分很好地解释了这些问题: http://en.wikipedia.org/wiki/XOR_swap_algorithm#Reasons_for_avoidance_in_practice

答案 1 :(得分:81)

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

Wiki页面上的更多信息: XOR swap algorithm

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

答案 2 :(得分:39)

在现代处理器上,您可以在对大型数组进行排序时使用以下内容,并且看不出速度差异:

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

你问题中真正重要的部分是'为什么?'部分。现在,回到2086年到8086天,上面将是一个真正的性能杀手,但在最新的奔腾,它将是你发布的两个匹配速度明智。

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

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

  • 缓存级别1 - 以与CPU相同的速度运行,访问时间可忽略不计,但很小
  • 缓存级别2 - 运行速度比L1慢一点但更大,访问开销更大(通常需要先将数据移到L1)
  • 缓存级别3 - (并非总是存在)通常在CPU外部,比L2
  • 更慢且更大
  • RAM - 主系统内存,通常实现管道,因此读取请求有延迟(CPU请求数据,发送到RAM的消息,RAM获取数据,RAM将数据发送到CPU)
  • 硬盘 - 当没有足够的RAM时,数据被分页到HD,这实际上很慢,而不是真的受CPU控制。

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

因此,优化交换方法实际上是毫无意义的 - 如果它只被调用几次,那么由于调用次数少而隐藏任何低效率,如果它被调用很多,则由于缓存未命中的数量而隐藏任何低效率(CPU需要从L2(1个周期),L3(10个周期),RAM(100个周期),HD(!))获取数据。

您真正需要做的是查看调用swap方法的算法。这不是一项微不足道的工作。尽管Big-O表示法很有用,但对于小n,O(n)可以明显快于O(log n)。 (我确定有一篇关于此问题的CodingHorror文章。)此外,许多算法都有退化的情况,其中代码执行的次数超过了必要条件(在几乎排序的数据上使用qsort可能比使用早期检查的冒泡排序慢)。因此,您需要分析算法及其使用的数据。

这导致如何分析代码。分析器很有用,但您需要知道如何解释结果。永远不要使用单次运行来收集结果,总是通过多次执行来得到平均结果 - 因为您的测试应用程序可能已被操作系统中途分页到硬盘。总是发布配置文件,优化的构建,分析调试代码是毫无意义的。

至于原来的问题 - 哪个更快? - 这就像试图通过观察后视镜的大小和形状来判断法拉利是否比Lambourgini更快。

答案 3 :(得分:13)

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

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

答案 4 :(得分:10)

@Harry:站在角落里想想你的建议。当你意识到自己的错误时,请回来。

从不将函数实现为宏,原因如下:

  1. 类型安全。空无一人。以下内容仅在编译时生成警告但在运行时失败:

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

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

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

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

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

  4. 编译器很聪明。我的意思是非常聪明。由非常聪明的人制作。他们可以做内联功能。即使在链接时(更聪明)。不要忘记内联会增加代码大小。大代码意味着在获取指令时更有可能出现缓存未命中,这意味着代码更慢。
  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++]);
    
  6. 宏:Just say no!

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

    EDIT2:回答Leahn Novash的评论:

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

    bytes = C(p) + C(f)
    

    其中C()给出产生的字节数,C(f)是函数的字节,C(p)是'housekeeping'代码的字节,编译器添加的前同步码和后同步码function(创建和销毁函数的堆栈帧等)。现在,调用函数f需要C(c)字节。如果函数被调用n次,则总代码大小为:

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

    现在让我们内联函数。 C(p),函数的'housekeeping'变为零,因为函数可以使用调用者的堆栈帧。 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;”)那么代码就会更大。

答案 5 :(得分:9)

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

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

答案 6 :(得分:7)

你正在优化错误的东西,这两者都应该如此之快,你必须运行数十亿次才能获得任何可衡量的差异。

几乎任何事情都会对你的表现产生更大的影响,例如,如果您交换的值在内存中接近您触及的最后一个值,那么它们将处于处理器缓存中,否则您将拥有访问内存 - 这比你在处理器内执行的任何操作慢几个数量级。

无论如何,你的瓶颈更可能是一个低效的算法或不恰当的数据结构(或通信开销),然后你如何交换数字。

答案 7 :(得分:6)

永远不理解对宏的仇恨。如果使用得当,它们可以使代码更紧凑和可读。我相信大多数程序员都知道应该谨慎使用宏,重要的是要明确特定的调用是宏而不是函数调用(全部大写)。如果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);
}

答案 8 :(得分:4)

真正了解的唯一方法是测试它,答案甚至可能因您所使用的编译器和平台而异。现代编译器现在非常善于优化代码,除非你能证明你的方式真的更快,否则你不应该试图超越编译器。

话虽如此,你最好有一个很好的理由选择#2而不是#1。 #1中的代码更具可读性,因此应始终首先选择。只有当你能证明你需要进行改变时才切换到#2 - 如果你这样做 - 请注释它以解释发生了什么以及为什么你这样做是非显而易见的。

作为一则轶事,我与一些的人合作过早地进行优化,这使得代码变得非常丑陋,难以维护。我也愿意打赌,他们往往会在脚下自我攻击,因为他们通过以非直接的方式编写代码来限制编译器优化代码的能力。

答案 9 :(得分:4)

除非你必须,我不会用指针做。由于pointer aliasing的可能性,编译器无法很好地优化它们(尽管如果你能保证指针指向不重叠的位置,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,则必须为每种类型制作单独的宏。

如果使用本地/全局变量作为参数调用上下文,优秀的编译器将尽可能积极地优化它。

答案 10 :(得分:4)

所有评价最高的答案实际上并不是确定的“事实”......他们是推测的人!

你可以明确知道一个事实哪些代码需要执行较少的汇编指令,因为你可以查看编译器生成的输出汇编,看看哪些汇编指令执行较少!

这是我用标志“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输出采用&gt;&gt;&gt; 11&lt;&lt;&lt;说明(不包括“离开”,“退回”,“大小”):

.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输出采用&gt;&gt;&gt; 11&lt;&lt;&lt;说明不包括“离开”和“退出”:

.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

装配输出摘要:
swap_traditional()需要11个指令
swap_xor()需要11条指令

结论:
两种方法都使用相同数量的指令来执行,因此在此硬件平台上的速度大致相同。

经验教训:
当你有小代码片段时,查看asm输出有助于快速迭代你的代码并提出最快(即最少的指令)代码。而且您可以节省时间,因为您不必为每次代码更改运行程序。您只需要使用分析器在最后运行代码更改,以显示代码更改更快。

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

答案 11 :(得分:3)

如上所述回答您的问题需要深入研究将运行此代码的特定CPU的指令时序,因此需要我围绕系统中的高速缓存状态和汇编代码做出一系列假设。由编译器发出。从了解您的选择处理器实际如何工作的角度来看,这将是一项有趣且有用的练习,但在现实世界中,这种差异可以忽略不计。

答案 12 :(得分:3)

对于现代CPU架构,方法1将更快,同时具有比方法2更高的可读性。

在现代CPU架构中,XOR技术比使用临时变量进行交换慢得多。一个原因是现代CPU努力通过指令流水线并行执行指令。在XOR技术中,每个操作的输入取决于前一个操作的结果,因此它们必须严格按顺序执行。如果效率非常令人担忧,建议测试XOR技术和目标架构上的临时变量交换的速度。查看here了解详情。


编辑:方法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;
    }
}

答案 13 :(得分:2)

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;

答案 14 :(得分:1)

在我看来,像这样的本地优化应该只被认为与平台紧密相关。如果您在16位uC编译器或以x64为目标的gcc上进行编译,则会产生巨大的差异。

如果您有一个特定的目标,那么只需尝试这两个目标并查看生成的asm代码或使用这两种方法分析您的应用程序,并了解哪种方法在您的平台上实际上更快。

答案 15 :(得分:0)

如果你可以使用一些内联汇编程序并执行以下操作(伪装配器):

PUSH A
A=B
POP B

您将节省大量参数传递和堆栈修复代码等。

答案 16 :(得分:-1)

我只是将两个掉期(作为宏)放在手写的快速排序中,我一直在玩。 XOR版本比临时变量(0.6秒)快得多(0.1秒)。然而,XOR确实破坏了数组中的数据(可能与Ant提到的地址相同)。

因为它是一个胖转子快速排序,XOR版本的速度可能来自使阵列的大部分相同。我尝试了第三版交换,这是最容易理解的,它与单个临时版本具有相同的时间。


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

[我只是在每个交换周围放置一个if语句,所以它不会尝试与自己交换,并且XOR现在与其他交换时间相同(0.6秒)]

答案 17 :(得分:-1)

如果您的编译器支持内联汇编程序且您的目标是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\r\n", a, b);
    return 0;
}

答案 18 :(得分:-2)

下面的代码也会这样做。此片段是优化的编程方式,因为它不使用任何第三个变量。

  x = x ^ y;
  y = x ^ y;
  x = x ^ y;

答案 19 :(得分:-3)

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

//我的C有点生疏,所以我希望我得到*对:)

答案 20 :(得分:-4)

另一种美丽的方式。

#define Swap( a, b ) (a)^=(b)^=(a)^=(b)

<强>优势

无需功能调用和方便。

<强>缺点:

当两个输入都是相同的变量时,这会失败。它只能用于整数变量。