加快Julia编写不佳的R示例的速度


77

朱莉娅比较R和性能的例子似乎特别令人费解https://github.com/JuliaLang/julia/blob/master/test/perf/perf.R

您可以从以下两种算法中获得最快的性能(最好是解释一下您所做的更改以使其更像R)?

## mandel

mandel = function(z) {
    c = z
    maxiter = 80
    for (n in 1:maxiter) {
        if (Mod(z) > 2) return(n-1)
        z = z^2+c
    }
    return(maxiter)
}

mandelperf = function() {
    re = seq(-2,0.5,.1)
    im = seq(-1,1,.1)
    M = matrix(0.0,nrow=length(re),ncol=length(im))
    count = 1
    for (r in re) {
        for (i in im) {
            M[count] = mandel(complex(real=r,imag=i))
            count = count + 1
        }
    }
    return(M)
}

assert(sum(mandelperf()) == 14791)

## quicksort ##

qsort_kernel = function(a, lo, hi) {
    i = lo
    j = hi
    while (i < hi) {
        pivot = a[floor((lo+hi)/2)]
        while (i <= j) {
            while (a[i] < pivot) i = i + 1
            while (a[j] > pivot) j = j - 1
            if (i <= j) {
                t = a[i]
                a[i] = a[j]
                a[j] = t
            }
            i = i + 1;
            j = j - 1;
        }
        if (lo < j) qsort_kernel(a, lo, j)
        lo = i
        j = hi
    }
    return(a)
}

qsort = function(a) {
  return(qsort_kernel(a, 1, length(a)))
}

sortperf = function(n) {
    v = runif(n)
    return(qsort(v))
}

sortperf(5000)


10
对于善良的缘故......让[R程序员编程R.
约翰

24
(1)这是R johnmyleswhite.com/notebook/2012/03/31/julia-i-love-you中的斐波那契示例,似乎他们正在使用它得出朱莉娅更快的结论,但在博客文章下面检查了我的评论我能够重写R解决方案(仍然仅使用纯R)并使其运行速度提高了2000倍。(2)通过字节编译可以使许多程序在R中更快地运行3到4倍,甚至不需要更改代码。(3)许多示例从一开始就针对R进行堆叠,因为它们使用递归,而R则不擅长。在组合中包含易于矢量化的问题会更加公平。
G. Grothendieck

7
@ G.Grothendieck您应该将您的评论发布为Answer Gabor;那里有很多相关点。+1
加文·辛普森

2
看到所有这些基准测试也扩展到Radford Neal的pqR可能会很有趣。
Spacedman

Answers:


44

嗯,在Mandelbrot的例子中,矩阵M的尺寸已转置

M = matrix(0.0,nrow=length(im), ncol=length(re))

因为它是通过增加count内部循环(的连续值im)来填充的。我的实现在mandelperf.1索引中创建了一个复数向量,并对所有元素进行运算,使用索引和子集来跟踪向量中哪些元素尚未满足条件Mod(z) <= 2

mandel.1 = function(z, maxiter=80L) {
    c <- z
    result <- integer(length(z))
    i <- seq_along(z)
    n <- 0L
    while (n < maxiter && length(z)) {
        j <- Mod(z) <= 2
        if (!all(j)) {
            result[i[!j]] <- n
            i <- i[j]
            z <- z[j]
            c <- c[j]
        }
        z <- z^2 + c
        n <- n + 1L
    }
    result[i] <- maxiter
    result
}

mandelperf.1 = function() {
    re = seq(-2,0.5,.1)
    im = seq(-1,1,.1)
    mandel.1(complex(real=rep(re, each=length(im)),
                     imaginary=im))
}

加快了13倍(结果相同但不完全相同,因为原始结果返回的是数字而不是整数)。

> library(rbenchmark)
> benchmark(mandelperf(), mandelperf.1(),
+           columns=c("test", "elapsed", "relative"),
+           order="relative")
            test elapsed relative
2 mandelperf.1()   0.412  1.00000
1   mandelperf()   5.705 13.84709

> all.equal(sum(mandelperf()), sum(mandelperf.1()))
[1] TRUE

quicksort示例实际上并未排序

> set.seed(123L); qsort(sample(5))
[1] 2 4 1 3 5

但是我的主要提速是围绕枢轴矢量化分区

qsort_kernel.1 = function(a) {
    if (length(a) < 2L)
        return(a)
    pivot <- a[floor(length(a) / 2)]
    c(qsort_kernel.1(a[a < pivot]), a[a == pivot], qsort_kernel.1(a[a > pivot]))
}

qsort.1 = function(a) {
    qsort_kernel.1(a)
}

sortperf.1 = function(n) {
    v = runif(n)
    return(qsort.1(v))
}

加速7倍(与未校正的原稿相比)

> benchmark(sortperf(5000), sortperf.1(5000),
+           columns=c("test", "elapsed", "relative"),
+           order="relative")
              test elapsed relative
2 sortperf.1(5000)    6.60 1.000000
1   sortperf(5000)   47.73 7.231818

由于在最初的比较中,Julia在Mandel上比R快30倍,在Quicksort上比R快500倍,因此上述实现仍然没有真正的竞争力。


@ gsk3字节编译并没有真正改变mandel的执行时间;sortperf.1比sortperf快约10倍而不是7倍。
马丁·摩根

问题是qsort_kernel.1仍然在进行递归,而R在这方面并不擅长。为了使其运行更快,请使用堆栈将其转换为循环。
G. Grothendieck

@ G.Grothendieck的递归不是那么深(log N),迭代解决方案可能使用堆栈(也不是R友好的),速度差仍然很大,并且算法不太直观。
马丁·摩根

1
在R中使用良好的计算数学库(例如Intel MKL)也有助于缩小R和Julia之间的差距。
aatrujillob

性能比较的目的不是要查看不同语言能够以多快的速度获得问题的解决方案,而是要比较同一实现(即算法)如何以不同语言执行。
bdeonovic '16

101

这个问题的关键词是“算法”:

您可以从以下两种算法中获得最快的性能(最好是解释一下您所做的更改以使其更像R)?

就像“您可以在R中使这些算法有多快?” 这里讨论的算法是标准的Mandelbrot复杂循环迭代算法和标准的递归快速排序内核。

当然,有更快的方法可以计算出这些基准测试中提出的问题的答案,但不能使用相同的算法。您可以避免递归,避免迭代,并避免其他R不擅长的事情。但是,您不再需要比较相同的算法。

如果您确实想计算R或排序编号中的Mandelbrot集,是的,这不是您编写代码的方式。您可能会对其进行尽可能的矢量化处理,从而将所有工作推入预定义的C内核中,或者只是编写自定义C扩展并在那里进行计算。无论哪种方式,结论都是R不够快,无法独自获得真正的良好性能-您需要让C做大部分工作才能获得良好的性能。

这正是这些基准测试的重点:在Julia中,您不必依赖C代码来获得良好的性能。您可以只用Julia编写您想做的事情,它会表现出色。如果迭代标量循环算法是执行您要执行的操作的最自然方法,则只需执行此操作即可。如果递归是解决问题的最自然的方法,那也没关系。无论是通过非自然矢量化还是编写自定义C扩展,都绝不会迫使您依靠C来提高性能。当然,您可以在自然的时候编写矢量化代码,就像在线性代数中那样。你可以调用C,如果你已经有了一些库,你想要做什么。但是您不必。

我们确实希望对各种语言的相同算法进行最公平的比较:

  1. 如果有人确实在R中使用相同算法的更快版本,请提交补丁!
  2. 我相信julia网站上的R基准已经编译过字节了,但是如果我做错了,并且比较对R不公平,请告诉我,我将对其进行修复并更新基准。

29
@Stefan好点。但是,相反的一点是,当您仅通过以一种语言中自然的方式编写代码而获得数百至数千倍的加速时,示例代码就是糟糕的R代码。如果有人来到SO并发布了这些示例代码,他们将很快得到关于如何编写R的教训,就像打算写R一样。假设所有示例似乎都涉及到递归(R被认为很差),然后示例代码就竭力避免向量化(R非常擅长)....
AriB。 Friedman

39
我仍然认为基准代码没有错。如果有一些R实现的迭代和递归速度很快,那么代码是否仍然很差?我能得出的唯一结论是,错误的语言实现而不是代码。此外,如果R语言的设计使快速进行迭代和递归特别具有挑战性(或可能是不可能的?),那么我想说这既不是该特定代码的错误,也不是当前R的实现,而是在语言设计本身。
StefanKarpinski

22
@Stefan我同意使用相同的算法是一个公平的比较,但也许值得将这些算法的Julia优化版本与任何R优化版本相同或尽可能重写为相同或近似。出现(即高度矢量化)并重新评估相对性能和针对Julia中基准代码的原始实现的性能的算法?最终,如果我可以使用针对另一种语言而优化的不同代码来获得相同的最终结果,那么这将是更快的代码。我非常感兴趣!
西蒙·奥汉隆

12
如果有可能编写一个使R中的递归或迭代速度更快的包,那么现在有人会做吗?诸如此类的基本语言改进不是您可以“坚持”的,它们需要深刻而困难的更改。这并不意味着不能在R中更快地进行递归和迭代,但是它并不是“仅仅是一个包”。
StefanKarpinski 2014年

29
@VB:这完全取决于您要测量的内容。就个人而言,我对人们能以多快的速度计算斐波纳契数不感兴趣。但这是我们的基准之一。为什么?因为我对语言如何支持递归非常感兴趣-双递归算法恰好是递归的很好测试,恰恰是因为它是一种计算斐波那契数的可怕方法。那么,将C和Julia中的故意慢速,过度递归算法与R中的棘手,聪明的矢量化算法进行比较,将会学到什么呢?没事
StefanKarpinski 2014年
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.