69

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

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

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

4

21 回答 21

106

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

坚持第 1 点,它是最通用和最容易理解的交换,并且可以轻松模板化/通用化。

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

于 2008-08-31T15:19:54.190 回答
90

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

Wiki 页面上的更多信息: XOR 交换算法

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

于 2008-08-31T16:17:17.577 回答
42

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

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

您问题中真正重要的部分是“为什么?” 部分。现在,回到 20 年前的 8086 天,以上将是一个真正的性能杀手,但在最新的 Pentium 上,这将是您发布的两者的匹配速度。

原因纯粹是内存,与CPU无关。

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

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

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

因此,优化 swap 方法真的毫无意义——如果它只被调用几次,那么由于调用次数少,任何低效率都会被隐藏,如果它被调用很多,那么由于缓存未命中的数量,任何低效率都会被隐藏(其中CPU 需要从 L2(1 个周期)、L3(10 个周期)、RAM(100 个周期)、HD(!))获取数据。

您真正需要做的是查看调用 swap 方法的算法。这不是一个简单的练习。尽管 Big-O 表示法很有用,但对于小 n,O(n) 可能比 O(log n) 快得多。(我敢肯定有一篇关于这方面的 CodingHorror 文章。)此外,许多算法都有退化的情况,其中代码的作用超出了必要的范围(对几乎有序的数据使用 qsort 可能比带有提前检查的冒泡排序慢)。因此,您需要分析您的算法及其使用的数据。

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

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

于 2008-09-05T10:30:45.857 回答
14

第一个更快,因为像 xor 这样的按位运算通常很难让读者看到。

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

于 2008-08-31T15:39:07.933 回答
11

关于@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 将被调用两次。

    编辑:具有令人讨厌的副作用的 AC 版本:

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

宏:说不!

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

EDIT2:回答 Leahn Novash 的评论:

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

bytes = C(p) + C(f)

其中 C() 给出生成的字节数,C(f) 是函数的字节数,C(p) 是“家务”代码的字节数,编译器添加到函数的前导码和后置码(创建并销毁函数的堆栈帧等)。现在,调用函数 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;”),那么代码会更大。

于 2008-09-05T15:58:11.060 回答
9

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

#define swap(a, b)   \
do {                 \
    int temp = a;    \
    a = b;           \
    b = temp;        \
} while(0)
于 2008-09-05T11:13:44.740 回答
8

从来不理解对宏的厌恶。如果使用得当,它们可以使代码更加紧凑和可读。我相信大多数程序员都知道应该小心使用宏,重要的是要明确特定调用是宏而不是函数调用(全部大写)。如果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);
}
于 2013-02-21T15:53:43.977 回答
7

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

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

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

于 2008-08-31T20:34:11.407 回答
5

真正知道的唯一方法是对其进行测试,答案甚至可能因您使用的编译器和平台而异。现代编译器现在真的很擅长优化代码,除非你能证明你的方法真的更快,否则你永远不应该试图超越编译器。

话虽如此,您最好有一个该死的充分理由选择#2而不是#1。#1 中的代码更具可读性,因此应始终首先选择。仅当您可以证明您需要进行更改时才切换到#2,并且如果您这样做了 - 评论它以解释正在发生的事情以及您为什么以非显而易见的方式进行更改。

作为轶事,我与几个喜欢过早优化的人一起工作,这会产生非常可怕的、不可维护的代码。我也愿意打赌,他们往往是在自责,因为他们阻碍了编译器通过以非直截了当的方式编写代码来优化代码的能力。

于 2008-08-31T15:58:03.023 回答
5

对于现代 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;
    }
}
于 2014-01-15T07:23:12.817 回答
4

除非你必须这样做,否则我不会用指针来做。由于指针别名的可能性,编译器无法很好地优化它们(尽管如果您可以保证指针指向不重叠的位置,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,则必须为每种类型创建一个单独的宏。

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

于 2008-10-01T01:44:33.527 回答
4

所有评分最高的答案实际上都不是确定的“事实”……他们是在猜测的人!

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

这是我用标志“gcc -std=c99 -S -O3lookingAtAsmOutput.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 <<< 指令(不包括“leave”、“ret”、“size”):

.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 <<< 不包括“leave”和“ret”的指令:

.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 代码,我经常使用这种方法。

于 2009-03-05T18:32:45.657 回答
3

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

于 2008-09-02T19:15:42.827 回答
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;
于 2017-08-23T15:54:55.880 回答
1

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

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

于 2008-10-10T12:11:07.837 回答
0

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

PUSH A
A=B
POP B

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

于 2008-08-31T16:34:17.490 回答
-1

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

由于它是一个胖枢轴快速排序,XOR 版本的速度可能来自于使数组的大部分相同。我尝试了第三个版本的交换,这是最容易理解的,它与单个临时版本具有相同的时间。


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

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

于 2008-09-04T22:41:10.103 回答
-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;
}
于 2009-03-22T17:03:23.497 回答
-2
void swap(int* a, int* b)
{
    *a = (*b - *a) + (*b = *a);
}

// 我的 C 有点生锈了,所以我希望我的 * 是对的 :)

于 2009-06-18T14:52:24.033 回答
-3

下面的代码将执行相同的操作。这个片段是优化的编程方式,因为它不使用任何第三个变量。

  x = x ^ y;
  y = x ^ y;
  x = x ^ y;
于 2015-11-09T04:26:55.083 回答
-4

另一种美丽的方式。

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

优势

无需函数调用,方便。

退税:

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

于 2009-10-07T17:57:05.300 回答