【问题标题】:What is the fastest way to swap values in C?在 C 中交换值的最快方法是什么?
【发布时间】:2010-09-07 09:43:48
【问题描述】:

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

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

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

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

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

【问题讨论】:

  • XOR 比较慢。使用godbolt 检查这两个函数的汇编指令计数。 注意如果你对值使用 XOR 方法而不是存储在指针下的值,速度是一样的(至少对于 GCC 编译器)
  • 似乎第一个使用了一个额外的寄存器这里有点晚了,但为什么会有人这么认为呢?比特旋转比使用临时变量更快的信念忽略了大多数计算机如何工作的现实,具有单独的 CPU 和内存。使用临时变量的交换可能实现为“将 A 加载到寄存器 1,将 B 加载到寄存器 2,将寄存器 1 保存到 B,将寄存器 2 保存到 A”。 “将两个变量加载到寄存器中,旋转一点,然后进行两次保存操作”速度较慢。 您必须同时加载并保存两者,一路上的小玩意是无关紧要的

标签: c performance


【解决方案1】:

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

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

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

【讨论】:

  • 正确。一般来说,最好向编译器说明你的目标,而不是试图欺骗它做你想做的事。 swap-with-temporary-variable 是一种常见的操作,任何体面的编译器都可以无情地优化它。
  • 我完全同意。此外,如果价值交换确实是一个瓶颈(通过测量证明)并且无法避免,请实施您能想到的所有方法并测量哪个更快 for you(您的机器、操作系统、编译器和应用程序)。低级的东西没有通用的答案。
  • 我的印象是swap,至少在 x86 上,实际上只是连续调用三个xors
  • @warren: xchg %eax, %eax 从字面上看就是标准的一字节 NOP 指令代码。它不会将 %eax 归零,因此它没有使用 xor。
  • @PeterCordes - 为什么 %eax 需要归零?
【解决方案2】:

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

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

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

【讨论】:

  • 通过添加条件 *a != *b 来防止混叠非常容易。
  • 那么你的交换函数有一个分支。尽管这是一个愚蠢的问题,但如果 OP 追求速度,那么引入分支可能是个坏主意。
  • @mamama,另外,它应该是 a != b 而不是 *a != *b;失败是地址是否相同,而不是值。
  • 也可以是 - 如果值已经相同,则不需要交换。但是检查 (a != b) 更有意义。
  • 如果有一些聪明的技巧可以加快这个速度,你的邻居编译器已经听说过它并且在你背后使用它。这种微优化(尤其是手动完成的)今天对您毫无用处,内存访问比执行指令慢很多。混淆代码以获得“性能”会损害等式中最昂贵的部分:程序员时间。
【解决方案3】:

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

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 级内存:

  • 缓存级别 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 可能比带有提前检查的冒泡排序慢)。因此,您需要分析您的算法及其使用的数据。

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

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

【讨论】:

  • +1 表示不必要的优化。如果您实际上已经分析了您的代码,并且您需要担心的最重要的事情是这两种交换一对整数的方法中哪一种更快,那么您已经编写了一个非常快的应用程序。在那之前,谁在乎交换?
  • @Ken White:我同意,而且,如果分析表明大部分时间都花在交换上,那很可能是因为你交换了太多次(冒泡排序任何人?),而不是缓慢交换。
  • 除了硬盘比 RAM 慢得多,进行交换还意味着您需要执行一些完全不同的代码,这些代码可能在 RAM 中,但几乎可以肯定不会在 L1 缓存中,并且很可能不在 L2 中(除非您严重缺少 RAM 并且不断地交换)。因此,在完成任何有用的操作之前,CPU 需要获取内存管理器代码中实际执行交换的部分。
  • 虽然您的基本观点是正确的,但您显示的代码比问题中给出的两个版本慢得多:Afaik,您在一个缓存行中获得四个 int,这意味着平均加载数据的延迟少于 30 个周期(不考虑预取),循环中有条件跳转(现代架构讨厌错误预测),因此每次循环迭代获得的不仅仅是一个周期。我敢打赌,你的交换至少需要 100 到 200 个周期,可能更多,但这在很大程度上取决于你交换的数字(有多少错误预测)。
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

    关于@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++]);
      

    宏:Just say no!

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

    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;”),那么代码会更大。

    【讨论】:

    • 您的副作用代码是 C++,而不是 C(C 中没有引用)。 C 程序员没有模板函数……它可能具有一些类型安全性,但对于解析和以其他方式实现来说绝对是一场噩梦。 C++ != C。它们有不同的抽象和约定类型和程度。
    【解决方案6】:

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

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

    【讨论】:

    • +1。当您需要速度时,这是在 C 中执行此操作的方法。如果你使用 GNU C 提供的 typeof() 扩展,这个宏甚至可以变得类型灵活。
    • Err... 为什么要使用不能自己内联的编译器?尽可能使用函数,必要时使用宏。函数是类型安全的,更容易理解。这个宏会用“swap(a++,b++)”做正确的事吗?函数会不会?
    • 如果您使用的是不错的编译器,您可以使用typeof(a)decltype(a) 来使其更通用。另外,一般来说,您应该添加括号以避免优先级问题(例如#define foo(a, b) bar(a, b, (a) + (b)))。
    • 这是一个可怕的解决方案。对于浮动,它将默默地失败。它也没有括号。
    • @John:从另一个答案中复制我的评论:typeof 通常可以让您编写避免多次评估其参数的宏。 #define SWAP_BY_REF(a,b) do{ typeof(a) _a = (a); typeof(b) _b = (b); typeof(*_a) tmp=*_a; *_a=*_b; *_b=tmp;}while(0)。或者你可以做_a=&amp;a,这样你就可以在值上使用它。希望编译器仍然可以优化将寄存器存储到内存中,这样他们就有了一个地址,用于交换两个已经存在于寄存器中的局部变量。 GNU libc 头文件在宏中大量使用_a=(a) 技巧;那是我第一次看到它的地方。
    【解决方案7】:

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

    【讨论】:

    • 被截断了?也许 SugarRichard 在大侦探的黄昏时会更合适。
    • 这比函数好多少?
    • typeof 通常允许您编写避免多次评估其参数的宏。 #define SWAP_BY_REF(a,b) do{ typeof(a) _a = (a); typeof(b) _b = (b); typeof(*_a) tmp=*_a; *_a=*_b; *_b=tmp;}while(0)。或者你可以做 _a=&a,所以你可以在值而不是指针上使用它。希望编译器仍然可以优化将寄存器存储到内存中,这样他们就有了一个地址,用于交换两个已经存在于寄存器中的局部变量。 GNU libc 头文件在宏中大量使用typeof(a) _a=(a) 技巧;那是我第一次看到它的地方。
    • @PeterCordes typeof 是 GCC 特定的扩展。
    【解决方案8】:

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

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

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

    【讨论】:

      【解决方案9】:

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

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

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

      【讨论】:

        【解决方案10】:

        除非您必须这样做,否则我不会使用指针。由于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,则必须为每种类型创建一个单独的宏。

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

        【讨论】:

        • 我喜欢你的回答。这是我想到的第一件事。您可能想为 c99 代码添加“寄存器”的使用,这也告诉编译器它们没有别名(如果程序员知道参数不是相同的对象,可以使用)
        【解决方案11】:

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

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

        这是我用标志“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

        .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
        

        汇编输出总结:
        swap_traditional() 需要 11 条指令
        swap_xor() 需要 11 条指令

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

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

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

        【讨论】:

        • 看起来你没有启用优化——局部变量在每个函数中被多次加载/存储。此外,在现代处理器中,您无法轻松计算周期数,因为任何涉及内存的东西都会占用可变数量的周期,具体取决于缓存是否命中。
        • 我确实使用“-o3”启用了优化,我什至使用了“restrict”关键字来确保编译器会优化。我还缺少什么? --- 可以说我计算的周期数不是绝对数。但我至少认为这将是一个相对计数?所以传统。方法仍然获胜?
        • -o3 表示“命名输出文件 3”。您需要 -O3(大写 O)。
        • 在流水线超标量(即当代)CPU 上,不能只计算汇编代码中的指令数并称之为“周期”。
        • “两种方法都使用相同数量的指令来执行,因此在此硬件平台上的速度大致相同。”因此什么?你的推理完全有缺陷。显然,速度不仅仅是指令数。
        【解决方案12】:

        对于现代 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;
            }
        }
        

        【讨论】:

        • 实际上,对于 +/- 就地交换,首先确保 a!=b 实际上并不重要。假设我们在声明 const 变量 const int C = *a 之前添加了一行,使得 C == *aC == *b 为真。然后:*a = *a + *b -> *a 等于 C+C*b = *a - *b -> *b 等于 C+C-C,即只是 C*a = *a - *b -> *a 等于 C+C-C,即只是 C; => *a == C, *b == C -> 好的
        • @Shillard 跳过不必要的交换可能并不重要,但很有用。 :P
        • 我不建议在您的代码中添加没有任何功能的逻辑分支。 (当然,如果您已经对其进行了速度测试以对您的特定情况有利,即 70+% 的时间a==b 或其他什么......但这是一个普遍的答案,因此没有特别的在这种情况下,最好省略逻辑分支。)代码中的“处理 a/b 共享相同引用的重要性”注释也不准确。
        【解决方案13】:

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

        【讨论】:

          【解决方案14】:

          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;
          

          【讨论】:

          • 这忽略了整数溢出的可能性和导致的未定义行为。
          【解决方案15】:

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

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

          【讨论】:

            【解决方案16】:

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

            PUSH A
            A=B
            POP B
            

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

            【讨论】:

            • 注意:vc++ 在 64 位模式下不允许内联汇编。希望它是相关的或被理解的:)
            • 交换两个寄存器的内容,而不是它们指向的位置。内联 ASM 还使编译器的优化能力大大降低,因此除非您为 SSE 指令执行此操作,或者您的内联 asm 包含内部循环,否则不值得这样做。
            • 在汇编中还有 xchg 命令,它交换两个值。
            • 什么是 nitpickin ...... 1) 伪代码,我并不是真的在推动寄存器 'A' blah blah。 2)同样,伪代码,不引用任何特定的汇编程序(xchg)。 3) 很多人不使用 64 位 vc++ (aaargh)。
            【解决方案17】:

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

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

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

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

            【讨论】:

            • 我喜欢这个评价! “它更快,但它确实破坏了数据。”经典。
            【解决方案18】:

            如果您的编译器支持内联汇编器并且您的目标是 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;
            }
            

            【讨论】:

            • 内联 ASM 使编译器更难优化。如果 xchg 更快,编译器就会使用它。不是,因为它有一个隐式锁定前缀。 (很慢)
            • 正确。我不知道这一点...感谢您启发我:)
            【解决方案19】:
            void swap(int* a, int* b)
            {
                *a = (*b - *a) + (*b = *a);
            }
            

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

            【讨论】:

              【解决方案20】:

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

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

              【讨论】:

              • 欢迎来到 SO!请注意,这个问题可以追溯到 2008 年(7 年前),并且您的答案已经是该问题的一部分。 OP 实际上是在询问速度性能,而不是内存。
              【解决方案21】:

              另一种美丽的方式。

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

              优势

              无需函数调用,方便。

              缺点:

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

              【讨论】:

                猜你喜欢
                • 2020-01-29
                • 1970-01-01
                • 2017-09-26
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-05-20
                • 2016-02-12
                • 2021-10-17
                相关资源
                最近更新 更多