JMH结果与算法复杂度解读

问题描述

我试图通过使用基准数据来证明算法的复杂性。我要测试的算法是二分搜索算法(规定的复杂度为 O(log n)),我想使用 JMH 库进行基准测试。

这是测试示例:

public class BinarySearchTest {

private static SearchAlgorithm binaryIterative = new BinarySearchIterative();
private static SearchAlgorithm binaryRecursive = new BinarySearchRecursive();

@Test
public void runBenchmarks() throws Exception {
    Options options = new OptionsBuilder()
            .include(this.getClass().getName() + ".*")
            .mode(Mode.Throughput)
            .forks(1)
            .threads(1)
            .warmupIterations(0)
            .measurementIterations(1)
            .shouldFailOnError(true)
            .shouldDoGC(true)
            .build();

    new Runner(options).run();
}

@Benchmark
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void binarySearchIterativeBenchmark(ExecutionPlan plan) {

    //given
    int size = randomPositiveIntLessThan(plan.arraySize);
    int[] array = generateUninterrupted(0,size);
    int target = randomPositiveIntLessThan(size);

    //when
    var result = binaryIterative.find(array,array.length,target);

    //then
    assertTrue(result != -1);
}

这是算法实现类:

public class BinarySearchIterative implements SearchAlgorithm {

@Override
public int find(int[] array,int start,int end,int target) {

    if (end > array.length) {
        return -1;
    }

    int left = start;
    int right = end;

    while (left <= right) {
        int median = left + (right - left) / 2;
        if (array[median] == target) {
            return median;
        }
        if (array[median] > target) {
            right = median - 1;
        }
        if (array[median] < target) {
            left = median + 1;
        }
    }
    return -1;
}

我使用带有 @State 注释的类来获取数组的大小:

@State(Scope.Benchmark)
public class ExecutionPlan {
    @Param({"100000","200000","300000","400000","500000","1000000","2000000","3000000","4000000","5000000","10000000","20000000","30000000","40000000","50000000"})
    public int arraySize;

所以我有下一个结果:

BinarySearchTest.binarySearchIterativeBenchmark 100000 thrpt
31.602 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 200000 thrpt 14.520 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 300000 thrpt
9.004 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 400000 thrpt 6.896 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 500000 thrpt
5.333 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 1000000 thrpt 2.304 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 2000000 thrpt
0.790 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 3000000 thrpt 0.451 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 4000000 thrpt
0.330 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 5000000 thrpt 0.232 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 10000000 thrpt
0.135 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 20000000 thrpt 0.061 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 30000000 thrpt
0.039 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 40000000 thrpt 0.033 ops/ms BinarySearchTest.binarySearchIterativeBenchmark 50000000 thrpt
0.025 次操作/毫秒

但是如果我绘制图形分数/数组大小,我得到的不是 log(n) 而是 1/x 图形。如果我使用 Mode.AverageTime,则图表相当x^2

这是我上面提供的数据图表,y[ms/ops],x[arraysize]:

enter image description here

如何从 JMH 获取操作单元或调整我的测试?

解决方法

你在绘制和比较错误的东西。

所以你得到了 每 1 毫秒的操作 ops_ms,这或多或少是测量时间 t(以 [ms] 为单位)除以 数量操作 m。对于大小的二分搜索n 是:

m = ~log2(n)

要获得复杂性和/或正确的绘图,您需要绘制测量时间 t 与大小 n 的关系,但是您绘制的是 ops_msn...

所以首先我们需要得到测得的时间ttop是单次操作的时间):

t = m*top
m = log2(n)
ops_ms = 1/top
--------------
top=1/ops_ms
t = log2(n)*top
--------------
t = log2(n)/ops_ms

因此您需要将 t 绘制为 y 轴,将 n 绘制为 x 轴。但是,正如您所看到的,这种测量方式毫无价值,因为您需要知道要测量什么才能获得 m,即使这只是一个近似值……更好/准确的方法是直接使用测量的时间作为你的 ops/ms 搞砸了。

当我在这样的数据上使用这个 measuring complexity 时:

const double l2=3.3219280948873623478703194294894;
double binsearch[]= // n[-],t[ms]
    {
      100000,l2*log(  100000.0)/31.602,200000,l2*log(  200000.0)/14.520,300000,l2*log(  300000.0)/ 9.004,400000,l2*log(  400000.0)/ 6.896,500000,l2*log(  500000.0)/ 5.333,1000000,l2*log( 1000000.0)/ 2.304,2000000,l2*log( 2000000.0)/ 0.790,3000000,l2*log( 3000000.0)/ 0.451,4000000,l2*log( 4000000.0)/ 0.330,5000000,l2*log( 5000000.0)/ 0.232,10000000,l2*log(10000000.0)/ 0.135,20000000,l2*log(20000000.0)/ 0.061,30000000,l2*log(30000000.0)/ 0.039,40000000,l2*log(40000000.0)/ 0.033,50000000,l2*log(50000000.0)/ 0.025,0.000
    };

它导致了这个结果:

binsearch O(n.log^4(n)) error = 0.398668

这仍然与预期的 log2(n) 相差太远,但比其他选项更接近。这意味着额外的东西正在压榨你的 ops/ms 值,这是我所期望的......你知道你有 JRE 架构,还有主机架构混淆了诸如缓存、预取管道等东西的测量,最重要的是你的JMH 也可能会做一些事情(例如出于某种目的平均或“增强” ops/ms 值)...

如果 ops_ms 实际上是 binsearch/ms,正如评论之一所建议的,那么时间由 1/ops_ms 计算,结果可能是真的稍微接近O(log(n)),但仍然太远:

//   time              O(n)          uncertainity
log2(n)/ops_ms    O(n.log^4(n))    error = 0.398668 // m ops / ms
    (n)/ops_ms    O(n^2.log^3(n))  error = 0.398668 // n ops / ms
    (1)/ops_ms    O(n.log^3(n))    error = 0.398668 // binsearch / ms

所以我的建议是找到一种直接测量时间的方法,而不是使用 ops/ms...

[edit1] 我测试过的 C++ 实现

int find(int *array,int size,int start,int end,int target)
    {
    if (end >= size) return -1;
    int left = start;
    int right = end;
    while (left <= right)
        {
        int median = left + (right - left) / 2;
        if (array[median] == target) return median;
        if (array[median] > target)  right = median - 1;
        if (array[median] < target)  left = median + 1;
        }
    return -1;
    }

用法:

const int n=50000000;
double binsearch[]= // n[-],1.0,0.000
    };
int *dat=new int[n],i,s;
Randomize();
for (s=0,i=0;i<n;i++)
    {
    s+=1+Random(10);
    dat[i]=s;
    }
for (i=0;binsearch[i];)
    {
    s=binsearch[i]; i++;
    tbeg(); // star measuring of time
    find(dat,s,s-1,dat[Random(s)]);
    tend(); // end measuring of time
    binsearch[i]=performance_tms; i++; // store measured time
    }
delete[] dat;

这将生成 PRNG int 升序数组并在其上测试您的 find 我敢打赌,正如我在评论中所述,您的数据根本不是随机的。当我应用它时,结果如预期:

binsearch O(log(n)) error = 0.528393

所以要么你的数组和/或目标没有正确选择,或者甚至你的时间测量包括它的生成,这把事情搞砸了。

如果我没看错,你的数组生成要么是 O(n^2) 要么是 O(n.log(n)) 反对我的 O(n) 所以如果它被包含在测量中,它将主导 O(log(n)) .. . 结果:

(1)/ops_ms    O(n.log^3(n))    error = 0.398668 // binsearch / ms

建议这种情况,使用的代数约为 O(n.log(n))log 功率差异只是由于使用的计算架构和时间测量不精确...

,

我想我已经找到了这种行为的原因。这是我的修复:

  1. 将基准测试模式更改为 Mode.AverageTime,因此现在基准测试输出以 ms/op 为单位的平均时间。
  2. 切换到纳秒 @OutputTimeUnit(TimeUnit.NANOSECONDS)
  3. 向基准添加了 1 个预热迭代。
  4. 将数组生成从测试移至 ExecutionPlan 类,并更改生成策略:现在生成随机整数值而不是连续整数数组(感谢@Spektre)
  5. 已将 @Setup 级别更改为 Level.Trial,根据 Level.Invocationdoc 用法有一些合理的警告。
  6. 添加了更多积分(现在为 30)。

这里是迭代二分查找得到的数据:

enter image description here

并用趋势线绘制图形:

enter image description here

有些点有很大的误差,但现在趋势趋向于O(log n)。我认为可以使用更多的迭代、热身和分叉来调整基准以获得更高的精度。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...