опубликован: 2024-07-12 19:21
последняя редакция: 2024-07-13 13:15
Вызываем фортран из перла
Как известно, слабое место Перла - математические вычисления. Меня всегда утешал тот факт, что в моих реальных задачах всякой арифметики не так уж и много. Но вот недавно задумался - а насколько медленно считает Перл на самом деле? И что, если его сравнить с признанным вычислительным лидером - фортраном?Сейчас, благодаря модулю FFI::Platypus, подключить многие языки к своему коду не такая уж и проблема. Давайте попробуем и посмотрим, в каких ситуациях имеет смысл переключаться на Fortran.
Все эксперименты я проводил на стареньком компьютере под линуксом, Debian 12. Потребуется установить пакет gfortran - я не думаю, что он самый быстрый из существующих фортранов, но самый доступный в моей ситуации. Итак, конфигурация тестового стенда такая:
$cat /proc/cpuinfo
# Intel(R) Core(TM) i3-4130 CPU @ 3.40GHz
$cat /proc/meminfo
# MemTotal: 8041264 kB
# MemFree: 1913888 kB
# MemAvailable: 3822652 kB
$perl -v
# This is perl 5, version 36, subversion 0 (v5.36.0) built for x86_64-linux-gnu-thread-multi
$gfortran --version
# GNU Fortran (Debian 12.2.0-14) 12.2.0
Попытка 1
Для начала попробуем сравнить какую-нибудь стандартную функцию, скажем синус. Помимо скрипта с перловым тестом, подготовим фортранную библиотеку, которую будем дёргать из этого теста. Если вы пока не знакомы с языком Fortran - не беда. Примеры будут выглядеть понятными.
c исходный файл: testlib.f
c компиляция: gfortran -shared testlib.f -o testlib.so
c test1
double precision function f77_sin(x)
double precision x
f77_sin = dsin(x)
end function f77_sin
Если всё прошло успешно, то в нашей рабочей директории должен появиться библиотечный файл testlib.so, который мы будем вызывать из перла.
Теперь напишем тестовый perl-скрипт. Экспериментальным путём определил, что для сравнения скоростей мне требуется по крайней мере 10 млн итераций. Ещё обратите внимание, что при вызове фортранной функции я указал в качестве аргумента не скаляр, а ссылку на него. Из прилагаемой документации на metacpan-е этот момент был для меня не слишком очевиден.
#! /usr/bin/perl
use strict;
use warnings;
use FFI::Platypus 2.00;
use Benchmark;
my $ffi = FFI::Platypus->new(
api => 2,
lang => 'Fortran',
lib => './testlib.so',
);
$ffi->attach(f77_sin => ['double*'] => 'double');
my $cycles = 10_000_000;
my $arg = 0.5;
sub perl_sin {
return sin($_[0]);
}
my $res = undef;
my $t0 = Benchmark->new;
for (1..$cycles) {
$res = perl_sin($arg);
}
my $t1 = Benchmark->new;
my $td = timediff($t1, $t0);
print "Perl:",timestr($td),"\n";
print "Result: $res\n\n";
#########################
$res = undef;
$t0 = Benchmark->new;
for (1..$cycles) {
$res = f77_sin(\$arg);
}
$t1 = Benchmark->new;
$td = timediff($t1, $t0);
print "Fortran:",timestr($td),"\n";
print "Result: $res\n";
Результат выполнения нашего теста такой:
Perl: 2 wallclock secs ( 1.30 usr + 0.00 sys = 1.30 CPU)
Result: 0.479425538604203
Fortran: 2 wallclock secs ( 2.26 usr + 0.00 sys = 2.26 CPU)
Result: 0.479425538604203
Пока не видно никаких преимуществ вызова фортрана из перла, по крайней мере для нативной функции. Очевидно, что накладные расходы на сторонний вызов пока перевешивают предполагаемые бонусы.
Попытка 2
Усложним немного задачу. Из школьного курса математики многие помнят, что синус в квадрате плюс косинус в квадрате всегда даёт единицу, для любого аргумента. Проверим это.
В наш фортранный исходник добавим следующие строчки:
c исходный файл: testlib.f
c test2
double precision function f77_sin2cos2(x)
double precision x
f77_sin2cos2 = dsin(x)**2 + dcos(x)**2
end function f77_sin2cos2
.. и заново перекомпилируем библиотеку gfortran -shared testlib.f -o testlib.so
Создадим новый тест:
#! /usr/bin/perl
use strict;
use warnings;
use FFI::Platypus 2.00;
use Benchmark;
my $ffi = FFI::Platypus->new(
api => 2,
lang => 'Fortran',
lib => './testlib.so',
);
$ffi->attach(f77_sin2cos2 => ['double*'] => 'double');
my $cycles = 10_000_000;
my $arg = 0.5;
sub perl_sin2cos2 {
return sin($_[0])**2 + cos($_[0])**2;
}
my $res = undef;
my $t0 = Benchmark->new;
for (1..$cycles) {
$res = perl_sin2cos2($arg);
}
my $t1 = Benchmark->new;
my $td = timediff($t1, $t0);
print "Perl:",timestr($td),"\n";
print "Result: $res\n\n";
#########################
$res = undef;
$t0 = Benchmark->new;
for (1..$cycles) {
$res = f77_sin2cos2(\$arg);
}
$t1 = Benchmark->new;
$td = timediff($t1, $t0);
print "Fortran:",timestr($td),"\n";
print "Result: $res\n";
И результат:
Perl: 3 wallclock secs ( 2.67 usr + 0.00 sys = 2.67 CPU)
Result: 1
Fortran: 2 wallclock secs ( 2.36 usr + 0.00 sys = 2.36 CPU)
Result: 1
Как видно, минимальное усложнение нашей задачи уже выводит фортран на первое место, хоть и чуть-чуть. Впрочем, пока повода замещать им перловую функцию не видно.
Попытка 3
Попробуем теперь сделать ещё один тест - на этот раз посчитаем полином восьмого порядка (произвольный, взятый из головы)
Итак,
c исходный файл: testlib.f
c test3
double precision function f77_polynom(x)
double precision x
f77_polynom = 100.d0 -5.d0*x + 7.d0*x**2 - x**3 - 6.5d0*x**4
& + 3.14d0*x**5 - 7.d0*x**6 - 6.4d0*x**7 - 0.1d0*x**8
end function f77_polynom
Тестовый скрипт:
#! /usr/bin/perl
use strict;
use warnings;
use FFI::Platypus 2.00;
use Benchmark;
my $ffi = FFI::Platypus->new(
api => 2,
lang => 'Fortran',
lib => './testlib.so',
);
$ffi->attach(f77_polynom => ['double*'] => 'double');
my $cycles = 10_000_000;
my $arg = 0.5;
sub perl_polynom {
return 100 -5*$_[0] + 7*$_[0]**2 - $_[0]**3 - 6.5*$_[0]**4
+ 3.14*$_[0]**5 - 7*$_[0]**6 - 6.4*$_[0]**7 - 0.1*$_[0]**8;
}
my $res = undef;
my $t0 = Benchmark->new;
for (1..$cycles) {
$res = perl_polynom($arg);
}
my $t1 = Benchmark->new;
my $td = timediff($t1, $t0);
print "Perl:",timestr($td),"\n";
print "Result: $res\n\n";
#########################
$res = undef;
$t0 = Benchmark->new;
for (1..$cycles) {
$res = f77_polynom(\$arg);
}
$t1 = Benchmark->new;
$td = timediff($t1, $t0);
print "Fortran:",timestr($td),"\n";
print "Result: $res\n";
И результат:
Perl: 6 wallclock secs ( 5.94 usr + 0.00 sys = 5.94 CPU)
Result: 98.657109375
Fortran: 2 wallclock secs ( 2.45 usr + 0.00 sys = 2.45 CPU)
Result: 98.657109375
Ну, наконец-то! Фортран всё-таки уделал Перл в два с хвостиком раза. Судя по всему, во всех тестах где-то 2 фортранных CPU - это накладные расходы на его вызов. Но вы помните, что мы прокручиваем 10 млн циклов теста? При единичном вызове функции во всех случаях величина CPU в районе абсолютного нуля.
Попытка 4
Давайте, для очистки совести, последний тест прогоним в другом режиме: десять миллионов циклов закрутим не внутри перла, а внутри фортрана и поймём, сколько стоит многократный вызов. А чтобы корректно сравнивать - перловую функцию также вызовем однократно, а цикл будет внутри функции:
c исходный файл: testlib.f
c test4
double precision function f77_cycpolynom(x)
double precision x, res
do i = 1, 10000000
res = 100.d0 -5.d0*x + 7.d0*x**2 - x**3 - 6.5d0*x**4
& + 3.14d0*x**5 - 7.d0*x**6 - 6.4d0*x**7 - 0.1d0*x**8
end do
f77_cycpolynom = res
end function f77_cycpolynom
Тестовый скрипт примет такую форму:
#! /usr/bin/perl
use strict;
use warnings;
use FFI::Platypus 2.00;
use Benchmark;
my $ffi = FFI::Platypus->new(
api => 2,
lang => 'Fortran',
lib => './testlib.so',
);
$ffi->attach(f77_cycpolynom => ['double*'] => 'double');
my $cycles = 10_000_000;
my $arg = 0.5;
sub perl_polynom {
my $res;
for (1..$cycles) {
$res = 100 -5*$_[0] + 7*$_[0]**2 - $_[0]**3 - 6.5*$_[0]**4
+ 3.14*$_[0]**5 - 7*$_[0]**6 - 6.4*$_[0]**7 - 0.1*$_[0]**8;
}
return $res
}
my $res = undef;
my $t0 = Benchmark->new;
$res = perl_polynom($arg);
my $t1 = Benchmark->new;
my $td = timediff($t1, $t0);
print "Perl:",timestr($td),"\n";
print "Result: $res\n\n";
#########################
$res = undef;
$t0 = Benchmark->new;
$res = f77_cycpolynom(\$arg);
$t1 = Benchmark->new;
$td = timediff($t1, $t0);
print "Fortran:",timestr($td),"\n";
print "Result: $res\n";
Итоговый результат:
Perl: 5 wallclock secs ( 4.83 usr + 0.00 sys = 4.83 CPU)
Result: 98.657109375
Fortran: 0 wallclock secs ( 0.26 usr + 0.00 sys = 0.26 CPU)
Result: 98.657109375
Преимущество фортрана при подсчёте полинома выросло с двух раз до почти двадцати!
Можем сделать вывод, что в бытовых задачах, для единичных расчётов, подключение фортрана к перловому коду не имеет особого смысла. Но дело в том, что в жизни программисту приходится решать не только бытовые задачи. Поэтому, оставляю вас с вашим здравым смыслом наедине.