我偶然发现了什么。起初我认为可能是分支错误预测的情况,如in this case,但我无法解释为什么分支错误预测应该导致这种现象。
我在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;
}
}
如您所见,这两种排序方法之间的唯一区别是>
与>=
。使用java BubbleSortAnnomaly 50000 10 10
运行程序时,您显然希望sortB
比sortA
慢,因为它必须执行更多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
的参数设置为50000
(java 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
不会更改结果。
答案 0 :(得分:43)
我认为这可能确实是由于分支预测。如果计算交换次数与内部排序迭代次数的比较,您会发现:
限制= 10
限制= 50000
因此在Limit == 10
情况下,交换在B排序中执行99.98%的时间,这明显有利于分支预测器。在Limit == 50000
情况下,交换仅随机命中68%,因此分支预测器的效益较小。
答案 1 :(得分:11)
我认为这确实可以用分支错误预测来解释。
例如,考虑LIMIT = 11和sortB
。在外循环的第一次迭代中,它将非常快速地偶然发现一个等于10的元素。因此它将具有a[j]=10
,因此绝对a[j]
将是>=a[next]
,因为它有没有大于10的元素。因此,它将执行交换,然后在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个中是假的。在分支预测失败时没有什么奇怪的。
答案 2 :(得分:9)
使用提供的C ++代码(删除时间计算)和perf stat
命令,我得到了确认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 :(得分:3)
编辑2:在大多数情况下这个答案可能是错误的,当我说上面的所有内容都是正确的时候,这个答案仍然是正确的,但对于大多数处理器架构来说,下半部分并不正确,请参阅注释。但是,我会说它理论上仍然 可能在某些操作系统/架构上有一些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>
上的文档为here
计算机比较数字的方式是从另一个中减去一个并检查该数字是否为0,因此在执行a < b
时如果从b
中减去a
并查看结果通过检查值的符号(b - a < 0
)小于0。要执行a <= b
虽然必须执行额外步骤并减去1(b - a - 1 < 0
)。
通常这是一个非常微小的差异,但这不是任何代码,这是怪异的冒泡排序! O(n ^ 2)是我们进行此特定比较的平均次数,因为它位于最内层循环中。
是的,它可能与分支预测有关我不确定,我不是这方面的专家,但我认为这也可能起到非显着作用。