如何才能提高Julia程序的性能以获得优秀的数字?

时间:2015-05-25 23:32:00

标签: performance julia

我一直在玩一些Perl programs to calculate excellent numbers。虽然我的解决方案的运行时间是可以接受的,但我认为另一种语言,尤其是专为数字设计的语言,可能会更快。一位朋友建议Julia,但我看到的表现非常糟糕我一定做错了。我已查看了Performance Tips并且没有看到我应该改进的内容:

digits = int( ARGS[1] )

const k = div( digits, 2 )

for a = ( 10 ^ (k - 1) ) : ( 10 ^ (k) - 1 )
    front = a * (10 ^ k + a)

    root = floor( front ^ 0.5 )

    for b = ( root - 1 ): ( root + 1 )
        back = b * (b - 1);
        if back > front
            break
        end
        if log(10,b) > k
            continue
        end
        if front == back
            @printf "%d%d\n" a b
        end
    end
 end

我有一个等效的C程序,速度快了一个数量级,而不是Julia page上注明的因子2(虽然关于Julia的速度的大多数Stackoverflow问题似乎都指向了该页面中有缺陷的基准测试):

我写的非优化的纯Perl只花了一半的时间:

use v5.20;

my $digits = $ARGV[0] // 2;
die "Number of digits must be even and non-zero! You said [$digits]\n"
    unless( $digits > 0 and $digits % 2 == 0 and int($digits) eq $digits );

my $k  = ( $digits / 2 );

foreach my $n ( 10**($k-1) .. 10**($k) - 1 ) {
    my $front = $n*(10**$k + $n);
    my $root = int( sqrt( $front ) );

    foreach my $try ( $root - 2 .. $root + 2 ) {
        my $back = $try * ($try - 1);
        last if length($try) > $k;
        last if $back > $front;
        # say "\tn: $n back: $back try: $try front: $front";
        if( $back == $front ) {
            say "$n$try";
            last;
            }
        }
    }

我使用预编译的Julia for Mac OS X,因为我无法获得编译源代码(但我没有尝试超过它第一次爆炸)。我认为这是它的一部分。

另外,我看到任何Julia程序的启动时间为0.7秒(参见Slow Julia Startup Time),这意味着在Julia完成一次之前,等效编译的C程序可以运行大约200次。随着运行时间的增加(digits的值越大)且启动时间越小,我的Julia程序仍然非常慢。

我还没有获得非常大数字(超过20位数的优秀数字)的部分,我没有意识到朱莉娅没有比其他大多数语言更好地处理这些数字。

这是我的C代码,与我开始时的情况略有不同。我生锈,不优雅的C技能与我的Perl基本相同。

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

int main( int argc, char *argv[] ) {
    long 
        k, digits,
        start, end,
        a, b,
        front, back,
        root
        ;

    digits = atoi( argv[1] );

    k = digits / 2;

    start = (long) pow(10, k - 1);
    end   = (long) pow(10, k);

    for( a = start; a < end; a++ ) {
        front = (long) a * ( pow(10,k) + a );
        root  = (long) floor( sqrt( front ) );

        for( b = root - 1; b <= root + 1; b++ ) {
            back = (long) b * ( b - 1 );
            if( back > front )  { break; }
            if( log10(b) > k ) { continue; }
            if( front == back ) {
                printf( "%ld%ld\n", a, b );
                }
            }
        }

    return 0;
    }

4 个答案:

答案 0 :(得分:17)

我已根据以下代码对您的代码(brian.jl)进行基准测试,该代码尝试对您的代码进行最少的更改并遵循Julian样式:

function excellent(digits)
    k = div(digits, 2)
    l = 10 ^ (k - 1)
    u = (10 ^ k) - 1

    for a in l:u
        front = a * (10 ^ k + a)
        root = isqrt(front)
        for b = (root - 1):(root + 1)
            back = b * (b - 1)
            back > front && break
            log10(b) > k && continue
            front == back && println(a,b)
        end
    end
end
excellent(int(ARGS[1]))

ul分开是个人对可读性的偏好。作为基准,我机器上的Julia启动时间是:

$ time julia -e ''
real    0m0.248s
user    0m0.306s
sys 0m0.091s

因此,如果从冷启动开始执行Julia时运行的计算大约为0.3秒,那么Julia在这个阶段可能不是一个很好的选择。我将16传递给了脚本,然后得到了:

$ time julia brian.jl 16
1045751633986928
1140820035650625
3333333466666668

real    0m15.973s
user    0m15.691s
sys     0m0.586s

$ time julia iain.jl 16
1045751633986928
1140820035650625
3333333466666668

real    0m9.691s
user    0m9.839s
sys     0m0.155s

编写此代码的限制是,如果digits>=20我们将超过Int64的存储空间。出于性能原因,Julia不会将整数类型自动提升为任意精度整数。我们可以通过将最后一行更改为:

来使用我们对问题的了解来解决这个问题
digits = int(ARGS[1])
excellent(digits >= 20 ? BigInt(digits) : digits)

我们免费获得优秀的BigInt版本,这很不错。暂时忽略这一点,在对我的版本进行概要分析时,我发现大约74%的时间用于计算log10,其次是isqrt的约19%。我通过用

替换最后一行来完成此分析
excellent(4)  # Warm up to avoid effects of JIT
@profile excellent(int(ARGS[1]))
Profile.print()

现在,如果我们想涉及一些较小的算法更改,根据我们现在从分析器中了解的内容,我们可以用{{1}替换log10行(只是检查有效位数) },这给了我们

ndigits(b) > k && continue

这会将余额从$ time julia iain.jl 16 1045751633986928 1140820035650625 3333333466666668 real 0m3.634s user 0m3.785s sys 0m0.153s 改为约~56%,从isqrt改变约28%。进一步挖掘到56%,大约一半用于执行this line这似乎是一个非常明智的算法,所以任何改进都可能改变比较的精神,因为它实际上是一个完全不同的方法。使用ndigits调查机器代码往往表明没有其他任何奇怪的事情发生,尽管我没有深入研究它。

如果我允许自己进行一些较小的算法改进,我可以从@code_native开始,只进行一次root+1检查,即

ndigits

让我失望

for a in l:u
    front = a * (10^k + a)
    root = isqrt(front)
    b = root + 1
    ndigits(b) > k && continue
    front == b*(b-1) && println(a,b)
    b = root
    front == b*(b-1) && println(a,b)
    b = root - 1
    front == b*(b-1) && println(a,b)
end

(我不相信需要进行后两次平等检查,但我正在努力减少差异!)。最后,我想通过预先计算real 0m2.901s user 0m3.050s sys 0m0.154s ,即10^k来增加一些额外的速度,这似乎是每次迭代时新计算的。有了这个,我到了

k10 = 10^k

与原始代码相比,这是一个相当不错的20倍。

答案 1 :(得分:11)

我很好奇Perl如何从这段代码中获得如此优异的性能,所以我觉得我应该做一个比较。由于问题中代码的Perl和Julia版本之间在控制流和操作方面存在一些看似不必要的差异,因此我将每个版本移植到另一种语言并将所有四种语言进行比较。我还使用了更多惯用数值函数编写了第五个Julia版本,但其控制流结构与问题的Perl版本相同。

第一个变体本质上是来自问题的Perl代码,但包含在函数中:

sub perl1 {
    my $k = $_[0];
    foreach my $n (10**($k-1) .. 10**($k)-1) {
        my $front = $n * (10**$k + $n);
        my $root = int(sqrt($front));

        foreach my $t ($root-2 .. $root+2) {
            my $back = $t * ($t - 1);
            last if length($t) > $k;
            last if $back > $front;
            if ($back == $front) {
                print STDERR "$n$t\n";
                last;
            }
        }
    }
}

接下来,我将其转换为Julia,保持相同的控制流并使用相同的操作 - 它在外部循环中取front的平方根的整数层,并取“字符串化”的长度在内循环中t

function julia1(k)
    for n = 10^(k-1):10^k-1
        front = n*(10^k + n)
        root = floor(Int,sqrt(front))

        for t = root-2:root+2
            back = t * (t - 1)
            length(string(t)) > k && break
            back > front && break
            if back == front
                println(STDERR,n,t)
                break
            end
        end
    end
end

这是问题的Julia代码,包含一些小的格式调整,包含在一个函数中:

function julia2(k)
    for a = 10^(k-1):10^k-1
        front = a * (10^k + a)
        root = floor(front^0.5)

        for b = root-1:root+1
            back = b * (b - 1);
            back > front && break
            log(10,b) > k && continue
            if front == back
                @printf STDERR "%d%d\n" a b
                # missing break?
            end
        end
    end
end

我将其转换回Perl,保持相同的控制流结构并使用与Perl代码相同的操作 - 将root的最低值提升到外部循环中的0.5次幂,并取对数基数10在内循环中:

sub perl2 {
    my $k = $_[0];
    foreach my $a (10**($k-1) .. 10**($k)-1) {
        my $front = $a * (10**$k + $a);
        my $root = int($front**0.5);

        foreach my $b ($root-1 .. $root+1) {
            my $back = $b * ($b - 1);
            last if $back > $front;
            next if log($b)/log(10) > $k;
            if ($front == $back) {
                print STDERR "$a$b\n"
            }
        }
    }
}

最后,我编写了一个Julia版本,它具有与问题的Perl版本相同的控制流结构,但使用了更多惯用的数值运算 - isqrtndigits函数:

function julia3(k)
    for n = 10^(k-1):10^k-1
        front = n*(10^k + n)
        root = isqrt(front)

        for t = root-2:root+2
            back = t * (t - 1)
            ndigits(t) > k && break
            back > front && break
            if back == front
                println(STDERR,n,t)
                break
            end
        end
    end
end

据我所知(我以前做过很多Perl编程,但已经有一段时间了),这些操作都没有Perl版本,因此没有相应的perl3变体。< / p>

我分别用Perl 5.18.2和Julia 0.3.9运行了所有五种变体,分别为2,4,6,8,10,12和14位数十次。以下是时间结果:

median execution time in seconds vs. digits

x轴是请求的位数。 y轴是计算每个函数所需的中值时间(秒)。 y轴以对数刻度绘制(在Gadfly的开罗后端存在一些渲染错误,因此上标不会显得很高。)我们可以看到,除了最小数量的数字(2)之外,所有三个Julia变体都比Perl变体更快 - 并且julia3比其他所有变量快得多。多快了?以下是相对于julia3的其他四个变体(不是对数刻度)的比较:

time relative to julia3 vs. digits

x轴是再次请求的位数,而y轴是每个变量比julia3慢多少倍。正如你在这里看到的,我无法重现问题中声称的Perl性能 - Perl代码不比Julia快2倍 - 比julia3慢7到40倍,比最慢慢至少2倍Julia变体用于任何非平凡的数字。我没有使用Perl 5.20测试 - 也许有人可以通过使用更新的Perl运行这些基准来跟进,看看是否能解释不同的结果?可以在此处找到运行基准测试的代码:excellent.plexcellent.jl。我像这样跑了他们:

cat /dev/null >excellent.csv
for d in 2 4 6 8 10 12 14; do
    perl excellent.pl $d >>excellent.csv
    julia excellent.jl $d >>excellent.csv
done

我使用excellent.csv分析了生成的this Julia script文件。

最后,正如评论中所提到的,使用BigIntInt128可以选择在Julia中探索更大的优秀数字。但是,这需要一点点关注一般编写算法。这是第四种一般性的变体:

function julia4(k)
    ten = oftype(k,10)
    for n = ten^(k-1):ten^k-1
        front = n*(ten^k + n)
        root = isqrt(front)

        for t = root-2:root+2
            back = t * (t - 1)
            ndigits(t) > k && break
            back > front && break
            if back == front
                println(STDERR,n,t)
                break
            end
        end
    end
end

这与julia3相同,但通过将10转换为其参数的类型,适用于通用整数类型。但是,由于算法以指数方式进行缩放,因此计算任何数字远大于14的数字仍然需要很长时间:

julia> @time julia4(int128(10)) # digits = 20
21733880705143685100
22847252005297850625
23037747345324014028
23921499005444619376
24981063345587629068
26396551105776186476
31698125906461101900
33333333346666666668
34683468346834683468
35020266906876369525
36160444847016852753
36412684107047802476
46399675808241903600
46401324208242096401
48179452108449381525
elapsed time: 2260.27479767 seconds (5144 bytes allocated)

它有效,但37分钟是一段很长的等待时间。使用更快的编程语言只能获得一个恒定的因子加速 - 在这种情况下是40倍 - 但只能购买几个额外的数字。要真正探索更大的优秀数字,您需要研究更好的算法。

答案 2 :(得分:4)

当我使用ifloor(未在Mathematical Operations and Elementary Functions中列出)而不是floor时,我获得了很大的加速。摆脱floor以支持isqrt显示相同的加速。我也没有看到记录在哪里。

现在我看到了我期望的性能,虽然在我的Mac上,看起来Julia无法启动k = 10. BigInt可以提供帮助,但性能却很糟糕。我看到朱莉娅的部分原因是我希望它可以轻松处理更大的数字,所以我必须继续关注它。

其他预期速度可能隐藏在对数算法的实现中,正如Colin在评论中指出的那样。

我很高兴看看这种语言,也许我会在它成熟时再试一次。

答案 3 :(得分:2)

您可以尝试将代码放入函数中。

function excellent(k)
  for a = ( 10 ^ (k - 1) ) : ( 10 ^ (k) - 1 )
    front = a * (10 ^ k + a)
    root = ifloor( sqrt(front) ) # floor() returns a double

    for b = ( root - 1 ): ( root + 1 )
      back = b * (b - 1);
      if back > front
        break
      end
      if log(10,b) > k
        continue
      end
      if front == back
        @printf "%d%d\n" a b
      end
   end
  end
end

@time excellent(7)
## 33333346666668
## 48484848484848
## elapsed time: 1.451842881 seconds (14680 bytes allocated)

对于任意精度数字,您可以使用BigInt(例如ten = BigInt("10"),但性能会下降......