冒泡排序中的> vs.> =会导致明显的性能差异


76

我偶然发现了一些东西。起初,我认为可能是这种情况下分支错误预测的情况,但是我无法解释为什么分支错误预测会导致这种行为。

我用Java实现了两个版本的Bubble Sort,并进行了一些性能测试:

import java.util.Random;

public class BubbleSortAnnomaly {

    public static void main(String... args) {
        final int ARRAY_SIZE = Integer.parseInt(args[0]);
        final int LIMIT = Integer.parseInt(args[1]);
        final int RUNS = Integer.parseInt(args[2]);

        int[] a = new int[ARRAY_SIZE];
        int[] b = new int[ARRAY_SIZE];
        Random r = new Random();
        for (int run = 0; RUNS > run; ++run) {
            for (int i = 0; i < ARRAY_SIZE; i++) {
                a[i] = r.nextInt(LIMIT);
                b[i] = a[i];
            }

            System.out.print("Sorting with sortA: ");
            long start = System.nanoTime();
            int swaps = bubbleSortA(a);

            System.out.println(  (System.nanoTime() - start) + " ns. "
                               + "It used " + swaps + " swaps.");

            System.out.print("Sorting with sortB: ");
            start = System.nanoTime();
            swaps = bubbleSortB(b);

            System.out.println(  (System.nanoTime() - start) + " ns. "
                               + "It used " + swaps + " swaps.");
        }
    }

    public static int bubbleSortA(int[] a) {
        int counter = 0;
        for (int i = a.length - 1; i >= 0; --i) {
            for (int j = 0; j < i; ++j) {
                if (a[j] > a[j + 1]) {
                    swap(a, j, j + 1);
                    ++counter;
                }
            }
        }
        return (counter);
    }

    public static int bubbleSortB(int[] a) {
        int counter = 0;
        for (int i = a.length - 1; i >= 0; --i) {
            for (int j = 0; j < i; ++j) {
                if (a[j] >= a[j + 1]) {
                    swap(a, j, j + 1);
                    ++counter;
                }
            }
        }
        return (counter);
    }

    private static void swap(int[] a, int j, int i) {
        int h = a[i];
        a[i] = a[j];
        a[j] = h;
    }
}

如我们所见,这两种排序方法之间的唯一区别是>vs >=.。当使用来运行程序时java BubbleSortAnnomaly 50000 10 10,很明显,它会sortBsortA因为它必须执行更多swap(...)s慢。但是我在三台不同的机器上得到了以下(或类似的)输出:

Sorting with sortA: 4.214 seconds. It used  564960211 swaps.
Sorting with sortB: 2.278 seconds. It used 1249750569 swaps.
Sorting with sortA: 4.199 seconds. It used  563355818 swaps.
Sorting with sortB: 2.254 seconds. It used 1249750348 swaps.
Sorting with sortA: 4.189 seconds. It used  560825110 swaps.
Sorting with sortB: 2.264 seconds. It used 1249749572 swaps.
Sorting with sortA: 4.17  seconds. It used  561924561 swaps.
Sorting with sortB: 2.256 seconds. It used 1249749766 swaps.
Sorting with sortA: 4.198 seconds. It used  562613693 swaps.
Sorting with sortB: 2.266 seconds. It used 1249749880 swaps.
Sorting with sortA: 4.19  seconds. It used  561658723 swaps.
Sorting with sortB: 2.281 seconds. It used 1249751070 swaps.
Sorting with sortA: 4.193 seconds. It used  564986461 swaps.
Sorting with sortB: 2.266 seconds. It used 1249749681 swaps.
Sorting with sortA: 4.203 seconds. It used  562526980 swaps.
Sorting with sortB: 2.27  seconds. It used 1249749609 swaps.
Sorting with sortA: 4.176 seconds. It used  561070571 swaps.
Sorting with sortB: 2.241 seconds. It used 1249749831 swaps.
Sorting with sortA: 4.191 seconds. It used  559883210 swaps.
Sorting with sortB: 2.257 seconds. It used 1249749371 swaps.

当将参数设置为时LIMIT,例如50000java BubbleSortAnnomaly 50000 50000 10),我得到了预期的结果:

Sorting with sortA: 3.983 seconds. It used  625941897 swaps.
Sorting with sortB: 4.658 seconds. It used  789391382 swaps.

我将程序移植到C ++,以确定此问题是否特定于Java。这是C ++代码。

#include <cstdlib>
#include <iostream>

#include <omp.h>

#ifndef ARRAY_SIZE
#define ARRAY_SIZE 50000
#endif

#ifndef LIMIT
#define LIMIT 10
#endif

#ifndef RUNS
#define RUNS 10
#endif

void swap(int * a, int i, int j)
{
    int h = a[i];
    a[i] = a[j];
    a[j] = h;
}

int bubbleSortA(int * a)
{
    const int LAST = ARRAY_SIZE - 1;
    int counter = 0;
    for (int i = LAST; 0 < i; --i)
    {
        for (int j = 0; j < i; ++j)
        {
            int next = j + 1;
            if (a[j] > a[next])
            {
                swap(a, j, next);
                ++counter;
            }
        }
    }
    return (counter);
}

int bubbleSortB(int * a)
{
    const int LAST = ARRAY_SIZE - 1;
    int counter = 0;
    for (int i = LAST; 0 < i; --i)
    {
        for (int j = 0; j < i; ++j)
        {
            int next = j + 1;
            if (a[j] >= a[next])
            {
                swap(a, j, next);
                ++counter;
            }
        }
    }
    return (counter);
}

int main()
{
    int * a = (int *) malloc(ARRAY_SIZE * sizeof(int));
    int * b = (int *) malloc(ARRAY_SIZE * sizeof(int));

    for (int run = 0; RUNS > run; ++run)
    {
        for (int idx = 0; ARRAY_SIZE > idx; ++idx)
        {
            a[idx] = std::rand() % LIMIT;
            b[idx] = a[idx];
        }

        std::cout << "Sorting with sortA: ";
        double start = omp_get_wtime();
        int swaps = bubbleSortA(a);

        std::cout << (omp_get_wtime() - start) << " seconds. It used " << swaps
                  << " swaps." << std::endl;

        std::cout << "Sorting with sortB: ";
        start = omp_get_wtime();
        swaps = bubbleSortB(b);

        std::cout << (omp_get_wtime() - start) << " seconds. It used " << swaps
                  << " swaps." << std::endl;
    }

    free(a);
    free(b);

    return (0);
}

该程序显示相同的行为。有人可以解释这里到底发生了什么吗?

sortB先执行然后再sortA执行不会更改结果。


1
您如何测量时间?如果仅测量一种情况的时间,则时间将在很大程度上取决于随机序列,而>vs>=仅会产生很小的影响。要获得真正有意义的数字,您必须测量许多不同的序列和平均值
idclev 463035818

@ tobi303看代码。您可以通过第三个运行时参数(Java)或-DRUNS=XXX(C ++,编译器指令)在循环中运行它。结果是可重复的。
图灵85年

3
您的C ++版本a在两次运行中都会排序(并且不会碰b)。
TC

@TC谢谢,纠正了错字。
Turing85

计算这两种情况下的交换次数以了解其与运行时之间的关系将非常有趣。我的意思是在情况A较慢的情况下,这绝对不是由于掉期的次数引起的,所以也许在情况A较快的情况下,原因也不只是掉期的次数而是更细微的影响
idclev 463035818 2015年

Answers:


45

我认为这确实可能是由于分支预测所致。如果将交换次数与内部排序迭代次数进行比较,则会发现:

限制= 10

  • A = 560M互换/ 1250M循环
  • B = 1250M交换/ 1250M循环(交换比循环少0.02%)

限制= 50000

  • A = 627M交换次数/ 1250M循环
  • B = 850M交换/ 1250M循环

因此,Limit == 10在B类中执行交换的时间为99.98%,这对于分支预测器显然是有利的。在这种Limit == 50000情况下,掉期仅随机发生68%,因此分支预测变量的收益较低。


2
您的论点似乎很明智。有什么方法可以检验您的假设吗?
Turing85

1
快速的答案是将输入数组控制为某种东西,以便A / B的排序以相同的顺序(至少大致)进行相同的交换。我不知道该怎么做。您还可以查看掉期顺序是“随机的”随机程度,并查看它是否与排序时间相关。
19:09 uesp

1
对于LIMIT >= ARRAY_SIZE可以执行测试用例的情况,其中数组由唯一数字组成。例如,如果a[i] = ARRAY_SIZE - i您在每个循环上交换一次,并且A / B排序的时间相同。
2015年

@ Turing85,请注意,我的回答实际上可以解释为什么互换次数不同。
彼得

@Petr为什么会有更多的互换对我来说很明显。我只是无法将这一事实与分支预测错误相关联。选择的答案(在我看来)以最佳的论据给出了最佳的解释。
Turing85

11

我认为这确实可以用分支错误预测来解释。

考虑例如LIMIT = 11和sortB。在外循环的第一次迭代中,它将很快偶然发现等于10的元素之一。由于没有大于10的元素,a[j]=10因此它将具有,因此肯定a[j]>=a[next]。因此,它将执行交换,然后再执行一步,j仅是a[j]=10再次找到该值(交换的值相同)。因此,它将再次变为a[j]>=a[next]等等。除了开始时的几个比较之外,所有比较都是正确的。同样,它将在外循环的下一个迭代中运行。

不一样的sortA。它将以大致相同的方式开始,偶然发现a[j]=10,以类似的方式进行一些交换,但是直到找到它为止a[next]=10。这样条件将为假,并且不会进行任何交换。依此类推:每次偶然发现时a[next]=10,条件为假,不进行任何交换。因此,此条件在11中有10次为真(a[next]0到9的值),在11中有1种情况为假。


9

使用perf stat命令提供的C ++代码(取消了计时),我得到的结果证实了brach-miss理论。

使用时Limit = 10,BubbleSortB从分支预测(未命中0.01%)中受益匪浅,但使用Limit = 50000分支预测失败(未命中15.65%)的结果甚至比在BubbleSortA中失败(未命中12.69%和12.76%)更多。

BubbleSortA限制= 10:

Performance counter stats for './bubbleA.out':

   46670.947364 task-clock                #    0.998 CPUs utilized          
             73 context-switches          #    0.000 M/sec                  
             28 CPU-migrations            #    0.000 M/sec                  
            379 page-faults               #    0.000 M/sec                  
117,298,787,242 cycles                    #    2.513 GHz                    
117,471,719,598 instructions              #    1.00  insns per cycle        
 25,104,504,912 branches                  #  537.904 M/sec                  
  3,185,376,029 branch-misses             #   12.69% of all branches        

   46.779031563 seconds time elapsed

BubbleSortA限制= 50000:

Performance counter stats for './bubbleA.out':

   46023.785539 task-clock                #    0.998 CPUs utilized          
             59 context-switches          #    0.000 M/sec                  
              8 CPU-migrations            #    0.000 M/sec                  
            379 page-faults               #    0.000 M/sec                  
118,261,821,200 cycles                    #    2.570 GHz                    
119,230,362,230 instructions              #    1.01  insns per cycle        
 25,089,204,844 branches                  #  545.136 M/sec                  
  3,200,514,556 branch-misses             #   12.76% of all branches        

   46.126274884 seconds time elapsed

BubbleSortB限制= 10:

Performance counter stats for './bubbleB.out':

   26091.323705 task-clock                #    0.998 CPUs utilized          
             28 context-switches          #    0.000 M/sec                  
              2 CPU-migrations            #    0.000 M/sec                  
            379 page-faults               #    0.000 M/sec                  
 64,822,368,062 cycles                    #    2.484 GHz                    
137,780,774,165 instructions              #    2.13  insns per cycle        
 25,052,329,633 branches                  #  960.179 M/sec                  
      3,019,138 branch-misses             #    0.01% of all branches        

   26.149447493 seconds time elapsed

BubbleSortB限制= 50000:

Performance counter stats for './bubbleB.out':

   51644.210268 task-clock                #    0.983 CPUs utilized          
          2,138 context-switches          #    0.000 M/sec                  
             69 CPU-migrations            #    0.000 M/sec                  
            378 page-faults               #    0.000 M/sec                  
144,600,738,759 cycles                    #    2.800 GHz                    
124,273,104,207 instructions              #    0.86  insns per cycle        
 25,104,320,436 branches                  #  486.101 M/sec                  
  3,929,572,460 branch-misses             #   15.65% of all branches        

   52.511233236 seconds time elapsed

3

编辑2:在大多数情况下,此答案可能是错误的,当我说上述所有内容都正确时,较低的要求仍然适用,但对于大多数处理器体系结构而言,较低的要求并不适用,请参见注释。但是,我要说的是,从理论上讲,仍然可能在某些OS /体系结构上有一些JVM来执行此操作,但是JVM的实现可能很差,或者它是一个怪异的体系结构。同样,从理论上说,最可能的事情在理论上是可能的,这在理论上是可行的,因此我将最后一部分加一粒盐。

首先,我不确定C ++,但是我可以谈谈Java。

这是一些代码,

public class Example {

    public static boolean less(final int a, final int b) {
        return a < b;
    }

    public static boolean lessOrEqual(final int a, final int b) {
        return a <= b;
    }
}

javap -c在它上面运行我得到字节码

public class Example {
  public Example();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static boolean less(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: if_icmpge     7
       5: iconst_1
       6: ireturn
       7: iconst_0
       8: ireturn

  public static boolean lessOrEqual(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: if_icmpgt     7
       5: iconst_1
       6: ireturn
       7: iconst_0
       8: ireturn
}

您会注意到,唯一的区别是if_icmpge(如果比较大于/等于)与if_icmpgt(如果比较大于)。

上面的一切都是事实,剩下的就是我对汇编语言的大学课程的了解if_icmpge以及如何if_icmpgt处理。为了获得更好的答案,您应该查看JVM如何处理这些问题。我的猜测是C ++也可以编译为类似的操作。

编辑:在文档if_i<cond>在这里

计算机比较数字的方式是将另一个数字相减,然后检查该数字是否为0,因此在进行a < bif减去ba,通过检查值的符号(b - a < 0)来查看结果是否小于0 。为此,a <= b它必须执行一个附加步骤并减去1(b - a - 1 < 0)。

通常这是一个很小的区别,但这不是任何代码,这是冒泡的!O(n ^ 2)是我们进行此特定比较的平均次数,因为它处于最内层循环中。

是的,这可能与分支预测有关,我不确定,我不是专家,但是我认为这可能也起着无关紧要的作用。


我不认为您<要比快要正确<=。处理器指令离散化;每条指令必须占用整数个时钟周期-除非您可以从中挤出整个时钟,否则没有“节省时间”。请参阅stackoverflow.com/a/12135533
kevinsa5年

请注意,我仅在谈论本机代码。我想JVM实现有可能执行“优化”,但是我猜想它只会使用本机指令,而不是自己编写解决方案。但这只是一个猜测。
kevinsa5

4
您基于什么来断言<=使用一个额外的步骤减去一个额外的1?例如,在x86级别,成功执行分支预测所允许的后跟a与cmp后跟ajl所花的时间完全相同。stackoverflow.com/questions/12135518/is-faster-than拥有更多详细信息。cmpjle
ClickRick

@ClickRick我学到的程序集用于SPARC,它使用了减少的指令集。也许没有jle?也许我也听到过这个错误的假设。我真的考虑过,现在还不能100%知道我现在从哪里得到的。从理论上讲,我认为尽管任何特定的OS /体系结构的JVM的解释方式可能会有所不同,但我现在假设它们都在一个周期内完成。
曼队长

2
@CaptainMan根据cs.northwestern.edu/~agupta/_projects/sparc_simulator/…,SPARC同时支持blble指令,这对我来说并不奇怪。
ClickRick
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.