背景

我想通过基准评估库中某些方法的出色表现。我不需要精度-足以表明某些东西是O(1),O(logn),O(n),O(nlogn),O(n ^ 2)或更糟糕的东西。由于big-oh表示上限,因此为O(log logn)估计O(logn)并不是问题。

现在,我正在考虑找到最适合每个big-oh数据的常数乘数k(但将以所有结果为最高),然后选择最适合的big-oh。

问题

  • 是否有比我想的更好的方法?如果是这样,它们是什么?
  • 否则,谁能指出我的算法来估计k以获得最佳拟合,并比较每条曲线对数据的拟合程度?

  • 注意事项和约束

    鉴于到目前为止的评论,我需要澄清一些事情:
  • 这需要自动化。我无法“看”数据并做出判断。
  • 我将使用多种n大小对这些方法进行基准测试。对于每种大小的n,我将使用一个经过验证的基准框架,该框架提供可靠的统计结果。
  • 我实际上实际上预先知道将要测试的大多数方法。我的主要目的是为他们提供性能回归测试。
  • 该代码将用Scala编写,并且可以使用任何免费的Java库。



  • 这是我要衡量的一种示例。我有一个带有此签名的方法:
    def apply(n: Int): A
    

    给定n,它将返回序列的第n个元素。在给定现有实现的情况下,此方法可以具有O(1),O(logn)或O(n),并且进行较小的更改就可以使它错误地使用次优的实现。或者,更容易地,可以得到其他依赖于它的方法来使用次优版本。

    最佳答案

    为了开始,您必须做几个假设。

  • n与任何常数项相比都很大。
  • 您可以有效地随机化输入数据
  • 您可以以足够的密度进行采样,以很好地掌握运行时的分布

  • 特别地,难以与(1)一起实现(3)。因此,您可能会遇到指数级最坏情况的情况,但永远不会遇到最坏情况,因此认为您的算法要比平均水平要好得多。

    话虽如此,您只需要任何标准曲线拟合库即可。 Apache Commons Math完全足够。然后,您可以使用要测试的所有常用术语创建一个函数(例如,常数,log n,n,n log n,nn,nn * n,e ^ n),也可以记录数据并拟合指数,然后如果您得到的指数不接近整数,则查看是否将log n进行拟合是否更合适。

    (更详细地讲,如果您将C*x^aCa配合使用,或更容易地为log C + a log x,则可以获取指数a;在所有一次项的通用方案中,您将获得每个项的权重,因此如果您有n*n + C*n*log(n),而C很大,您也将选择该术语。)

    您将需要适当地改变大小,以便可以区分不同的情况(如果要注意日志项,可能很难做到),并且可以安全地使用比参数更多的大小(可能开始超过3倍)好的,只要您总共至少运行一打左右即可。

    编辑:这是Scala代码,可以为您完成所有这些操作。我不会解释每个小片段,而是交给您调查。它使用C * x ^ a fit实现上述方案,并返回((a,C),(a的下限,a的上限))。从运行几次可以看到,边界是相当保守的。 C的单位是秒(a是无单位的),但是不要过多地相信它,因为存在一些循环开销(也有一些噪音)。
    class TimeLord[A: ClassManifest,B: ClassManifest](setup: Int => A, static: Boolean = true)(run: A => B) {
      @annotation.tailrec final def exceed(time: Double, size: Int, step: Int => Int = _*2, first: Int = 1): (Int,Double) = {
        var i = 0
        val elapsed = 1e-9 * {
          if (static) {
            val a = setup(size)
            var b: B = null.asInstanceOf[B]
            val t0 = System.nanoTime
            var i = 0
            while (i < first) {
              b = run(a)
              i += 1
            }
            System.nanoTime - t0
          }
          else {
            val starts = if (static) { val a = setup(size); Array.fill(first)(a) } else Array.fill(first)(setup(size))
            val answers = new Array[B](first)
            val t0 = System.nanoTime
            var i = 0
            while (i < first) {
              answers(i) = run(starts(i))
              i += 1
            }
            System.nanoTime - t0
          }
        }
        if (time > elapsed) {
          val second = step(first)
          if (second <= first) throw new IllegalArgumentException("Iteration size increase failed: %d to %d".format(first,second))
          else exceed(time, size, step, second)
        }
        else (first, elapsed)
      }
    
      def multibench(smallest: Int, largest: Int, time: Double, n: Int, m: Int = 1) = {
        if (m < 1 || n < 1 || largest < smallest || (n>1 && largest==smallest)) throw new IllegalArgumentException("Poor choice of sizes")
        val frac = (largest.toDouble)/smallest
        (0 until n).map(x => (smallest*math.pow(frac,x/((n-1).toDouble))).toInt).map{ i =>
          val (k,dt) = exceed(time,i)
          if (m==1) i -> Array(dt/k) else {
            i -> ( (dt/k) +: (1 until m).map(_ => exceed(time,i,first=k)).map{ case (j,dt2) => dt2/j }.toArray )
          }
        }.foldLeft(Vector[(Int,Array[Double])]()){ (acc,x) =>
          if (acc.length==0 || acc.last._1 != x._1) acc :+ x
          else acc.dropRight(1) :+ (x._1, acc.last._2 ++ x._2)
        }
      }
    
      def alpha(data: Seq[(Int,Array[Double])]) = {
        // Use Theil-Sen estimator for calculation of straight-line fit for exponent
        // Assume timing relationship is t(n) = A*n^alpha
        val dat = data.map{ case (i,ad) => math.log(i) -> ad.map(x => math.log(i) -> math.log(x)) }
        val slopes = (for {
          i <- dat.indices
          j <- ((i+1) until dat.length)
          (pi,px) <- dat(i)._2
          (qi,qx) <- dat(j)._2
        } yield (qx - px)/(qi - pi)).sorted
        val mbest = slopes(slopes.length/2)
        val mp05 = slopes(slopes.length/20)
        val mp95 = slopes(slopes.length-(1+slopes.length/20))
        val intercepts = dat.flatMap{ case (i,a) => a.map{ case (li,lx) => lx - li*mbest } }.sorted
        val bbest = intercepts(intercepts.length/2)
        ((mbest,math.exp(bbest)),(mp05,mp95))
      }
    }
    

    请注意,假设使用了静态初始化数据,并且与您正在运行的东西相比,multibench方法运行大约需要sqrt(2)nm *时间才能运行。以下是一些示例,这些示例选择的参数需要大约15秒才能运行:
    val tl1 = new TimeLord(x => List.range(0,x))(_.sum)  // Should be linear
    // Try list sizes 100 to 10000, with each run taking at least 0.1s;
    // use 10 different sizes and 10 repeats of each size
    scala> tl1.alpha( tl1.multibench(100,10000,0.1,10,10) )
    res0: ((Double, Double), (Double, Double)) = ((1.0075537890632216,7.061397125245351E-9),(0.8763463348353099,1.102663784225697))
    
    val longList = List.range(0,100000)
    val tl2 = new TimeLord(x=>x)(longList.apply)    // Again, should be linear
    scala> tl2.alpha( tl2.multibench(100,10000,0.1,10,10) )
    res1: ((Double, Double), (Double, Double)) = ((1.4534378213477026,1.1325696181862922E-10),(0.969955396265306,1.8294175293676322))
    
    // 1.45?!  That's not linear.  Maybe the short ones are cached?
    scala> tl2.alpha( tl2.multibench(9000,90000,0.1,100,1) )
    res2: ((Double, Double), (Double, Double)) = ((0.9973235607566956,1.9214696731124573E-9),(0.9486294398193154,1.0365312207345019))
    
    // Let's try some sorting
    val tl3 = new TimeLord(x=>Vector.fill(x)(util.Random.nextInt))(_.sorted)
    scala> tl3.alpha( tl3.multibench(100,10000,0.1,10,10) )
    res3: ((Double, Double), (Double, Double)) = ((1.1713142886974603,3.882658025586512E-8),(1.0521099621639414,1.3392622111121666))
    // Note the log(n) term comes out as a fractional power
    // (which will decrease as the sizes increase)
    
    // Maybe sort some arrays?
    // This may take longer to run because we have to recreate the (mutable) array each time
    val tl4 = new TimeLord(x=>Array.fill(x)(util.Random.nextInt), false)(java.util.Arrays.sort)
    scala> tl4.alpha( tl4.multibench(100,10000,0.1,10,10) )
    res4: ((Double, Double), (Double, Double)) = ((1.1216172965292541,2.2206198821180513E-8),(1.0929414090177318,1.1543697719880128))
    
    // Let's time something slow
    def kube(n: Int) = (for (i <- 1 to n; j <- 1 to n; k <- 1 to n) yield 1).sum
    val tl5 = new TimeLord(x=>x)(kube)
    scala> tl5.alpha( tl5.multibench(10,100,0.1,10,10) )
    res5: ((Double, Double), (Double, Double)) = ((2.8456382116915484,1.0433534274508799E-7),(2.6416659356198617,2.999094292838751))
    // Okay, we're a little short of 3; there's constant overhead on the small sizes
    

    无论如何,对于指定的用例(正在检查以确保顺序没有变化),这可能就足够了,因为在设置测试时您可以使用一些值以确保它们提供了合理的信息。人们还可以创建探索稳定性的启发式方法,但这可能是过大了。

    (顺便说一下,这里没有明确的热身步骤; Theil-Sen估算器的强大拟合功能应该使其对于合理的大型基准没有必要。这也是为什么我不使用任何其他基准测试框架的原因;它失去的任何统计信息测试的力量。)

    再次编辑:如果将alpha方法替换为以下内容:
      // We'll need this math
      @inline private[this] def sq(x: Double) = x*x
      final private[this] val inv_log_of_2 = 1/math.log(2)
      @inline private[this] def log2(x: Double) = math.log(x)*inv_log_of_2
      import math.{log,exp,pow}
    
      // All the info you need to calculate a y value, e.g. y = x*m+b
      case class Yp(x: Double, m: Double, b: Double) {}
    
      // Estimators for data order
      //   fx = transformation to apply to x-data before linear fitting
      //   fy = transformation to apply to y-data before linear fitting
      //   model = given x, slope, and intercept, calculate predicted y
      case class Estimator(fx: Double => Double, invfx: Double=> Double, fy: (Double,Double) => Double, model: Yp => Double) {}
      // C*n^alpha
      val alpha = Estimator(log, exp, (x,y) => log(y), p => p.b*pow(p.x,p.m))
      // C*log(n)*n^alpha
      val logalpha = Estimator(log, exp, (x,y) =>log(y/log2(x)), p => p.b*log2(p.x)*pow(p.x,p.m))
    
      // Use Theil-Sen estimator for calculation of straight-line fit
      case class Fit(slope: Double, const: Double, bounds: (Double,Double), fracrms: Double) {}
      def theilsen(data: Seq[(Int,Array[Double])], est: Estimator = alpha) = {
        // Use Theil-Sen estimator for calculation of straight-line fit for exponent
        // Assume timing relationship is t(n) = A*n^alpha
        val dat = data.map{ case (i,ad) => ad.map(x => est.fx(i) -> est.fy(i,x)) }
        val slopes = (for {
          i <- dat.indices
          j <- ((i+1) until dat.length)
          (pi,px) <- dat(i)
          (qi,qx) <- dat(j)
        } yield (qx - px)/(qi - pi)).sorted
        val mbest = slopes(slopes.length/2)
        val mp05 = slopes(slopes.length/20)
        val mp95 = slopes(slopes.length-(1+slopes.length/20))
        val intercepts = dat.flatMap{ _.map{ case (li,lx) => lx - li*mbest } }.sorted
        val bbest = est.invfx(intercepts(intercepts.length/2))
        val fracrms = math.sqrt(data.map{ case (x,ys) => ys.map(y => sq(1 - y/est.model(Yp(x,mbest,bbest)))).sum }.sum / data.map(_._2.length).sum)
        Fit(mbest, bbest, (mp05,mp95), fracrms)
      }
    

    那么当还有一个对数项时,您可以得到指数的估计值–存在误差估计值以选择对数项是否是正确的处理方法,但这取决于您进行调用(即,我假设您将首先对此进行监督并阅读得出的数字):
    val tl3 = new TimeLord(x=>Vector.fill(x)(util.Random.nextInt))(_.sorted)
    val timings = tl3.multibench(100,10000,0.1,10,10)
    
    // Regular n^alpha fit
    scala> tl3.theilsen( timings )
    res20: tl3.Fit = Fit(1.1811648421030059,3.353753446942075E-8,(1.1100382697696545,1.3204652930525234),0.05927994882343982)
    
    // log(n)*n^alpha fit--note first value is closer to an integer
    //   and last value (error) is smaller
    scala> tl3.theilsen( timings, tl3.logalpha )
    res21: tl3.Fit = Fit(1.0369167329732445,9.211366397621766E-9,(0.9722967182484441,1.129869067913768),0.04026308919615681)
    

    (编辑:修正了RMS计算,因此它实际上是平均值,并证明您只需要进行一次计时,然后可以尝试两次拟合。)

    关于java - 凭经验估计大时间效率,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/8836393/

    10-13 03:53