我一直在玩一些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;
}
答案 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]))
将u
和l
分开是个人对可读性的偏好。作为基准,我机器上的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版本相同的控制流结构,但使用了更多惯用的数值运算 - isqrt
和ndigits
函数:
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位数十次。以下是时间结果:
x轴是请求的位数。 y轴是计算每个函数所需的中值时间(秒)。 y轴以对数刻度绘制(在Gadfly的开罗后端存在一些渲染错误,因此上标不会显得很高。)我们可以看到,除了最小数量的数字(2)之外,所有三个Julia变体都比Perl变体更快 - 并且julia3
比其他所有变量快得多。多快了?以下是相对于julia3
的其他四个变体(不是对数刻度)的比较:
x轴是再次请求的位数,而y轴是每个变量比julia3
慢多少倍。正如你在这里看到的,我无法重现问题中声称的Perl性能 - Perl代码不比Julia快2倍 - 比julia3
慢7到40倍,比最慢慢至少2倍Julia变体用于任何非平凡的数字。我没有使用Perl 5.20测试 - 也许有人可以通过使用更新的Perl运行这些基准来跟进,看看是否能解释不同的结果?可以在此处找到运行基准测试的代码:excellent.pl,excellent.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文件。
最后,正如评论中所提到的,使用BigInt
或Int128
可以选择在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")
,但性能会下降......