benchmark.pl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. #!/usr/bin/perl
  2. # Author: Daniel "Trizen" Șuteu
  3. # License: GPLv3
  4. # Date: 14 February 2015
  5. # Edit: 15 February 2015
  6. # Website: http://github.com/trizen
  7. # Speed comparison of common programming languages.
  8. # The results are stored in separate CSV files for each test.
  9. use 5.014;
  10. use strict;
  11. use autodie;
  12. use warnings;
  13. use Text::CSV qw();
  14. use Text::ParseWords qw(quotewords);
  15. use File::Path qw(make_path);
  16. use File::Temp qw(mktemp tempfile tempdir);
  17. use File::Basename qw(basename);
  18. use File::Spec::Functions qw(rel2abs catfile catdir tmpdir curdir updir);
  19. use List::Util qw(min max sum);
  20. use Getopt::Long qw(GetOptions);
  21. use Time::HiRes qw(gettimeofday tv_interval);
  22. # The main directories which contain sub-directories with source files
  23. my $compiled_langs_dir = 'Compiled';
  24. my $interpreted_langs_dir = 'Interpreted';
  25. # The test specific files
  26. my $arguments_file = 'args.txt';
  27. my $ignore_file = 'ignore.txt';
  28. # The directories where the reports are written
  29. my $reports_dir = 'Reports';
  30. my $compiled_reports_dir = catdir($reports_dir, 'Compiled');
  31. my $interpreted_reports_dir = catdir($reports_dir, 'Interpreted');
  32. # The compilers and interpreters definitions
  33. my $compilers = do(rel2abs(catfile($compiled_langs_dir, 'compilers.txt')));
  34. my $interpreters = do(rel2abs(catfile($interpreted_langs_dir, 'interpreters.txt')));
  35. # Flags
  36. my $test_compiled = 0;
  37. my $test_interpreted = 0;
  38. my $repeat_n = 3;
  39. my $test_loadtime = 0;
  40. my $test_name = '';
  41. sub help {
  42. print <<"USAGE";
  43. usage: $0 [options]
  44. options:
  45. -i : test interpreted languages
  46. -c : test compiled languages
  47. -t name : run only a specific test
  48. -r int : repeat each test this many times (default: $repeat_n)
  49. -l : time loadtime and subtract it from runtime (default: $test_loadtime)
  50. example:
  51. $0 -i -r 1 -t "fibonacci-recursive"
  52. USAGE
  53. exit;
  54. }
  55. GetOptions(
  56. 'i!' => \$test_interpreted,
  57. 'c!' => \$test_compiled,
  58. 'r=i' => \$repeat_n,
  59. 't=s' => \$test_name,
  60. 'l!' => \$test_loadtime,
  61. 'help|h' => \&help,
  62. )
  63. or die("Error in command line arguments!");
  64. sub create_cmd {
  65. my ($cmd, $in, $out) = @_;
  66. my @new_cmd;
  67. foreach my $field (@{$cmd}) {
  68. if (defined $in) {
  69. if ($field eq '%in' or $field eq '%in%') {
  70. push @new_cmd, $in;
  71. next;
  72. }
  73. }
  74. if (defined $out) {
  75. if ($field eq '%out' or $field eq '%out%') {
  76. push @new_cmd, $out;
  77. next;
  78. }
  79. }
  80. push @new_cmd, $field;
  81. }
  82. return @new_cmd;
  83. }
  84. sub get_dir_entries {
  85. my ($dirname) = @_;
  86. opendir(my $dir_h, $dirname);
  87. map { $_ => catfile($dirname, $_) }
  88. grep { $_ ne curdir() and $_ ne updir() } readdir($dir_h);
  89. }
  90. sub files_by_ext {
  91. my ($ext_array, $files_array) = @_;
  92. my @files;
  93. foreach my $file (@{$files_array}) {
  94. foreach my $ext (@{$ext_array}) {
  95. if ($file =~ /\.\Q$ext\E\z/i) {
  96. push @files, $file;
  97. last;
  98. }
  99. }
  100. }
  101. return @files;
  102. }
  103. sub get_arguments {
  104. my ($files_array) = @_;
  105. my @args;
  106. foreach my $file (@{$files_array}) {
  107. if (basename($file) eq $arguments_file) {
  108. open my $fh, '<:utf8', $file;
  109. push @args, quotewords(qr/\s+/, 0, unpack('A*', scalar(<$fh>)));
  110. last;
  111. }
  112. }
  113. return @args;
  114. }
  115. sub get_ignores {
  116. my ($files_array) = @_;
  117. my %ignore;
  118. foreach my $file (@{$files_array}) {
  119. if (basename($file) eq $ignore_file) {
  120. open my $fh, '<:utf8', $file;
  121. while (defined(my $line = <$fh>)) {
  122. $ignore{unpack 'A*', $line} = 1;
  123. }
  124. last;
  125. }
  126. }
  127. return %ignore;
  128. }
  129. sub map_files_to_dirs {
  130. my (%entries) = @_;
  131. my %files;
  132. while (my ($key, $dir) = each %entries) {
  133. if (-d $dir) {
  134. my %dir_entries = get_dir_entries($dir);
  135. my @files = map { $dir_entries{$_} } grep { -f $dir_entries{$_} } keys %dir_entries;
  136. @files ? (push @{$files{$key}}, @files) : ();
  137. }
  138. }
  139. return %files;
  140. }
  141. sub execute_cmd {
  142. system(@_) == 0;
  143. }
  144. sub time_cmd {
  145. my ($seconds, $microseconds) = gettimeofday();
  146. execute_cmd(@_) || return -1;
  147. tv_interval([$seconds, $microseconds], [gettimeofday()]);
  148. }
  149. sub mMavg {
  150. (min(@_), max(@_), sum(@_) / @_);
  151. }
  152. sub write_report {
  153. my ($report_ref, $report_dir) = @_;
  154. my $csv = Text::CSV->new(
  155. {
  156. eol => "\n",
  157. binary => 1,
  158. sep_char => ',',
  159. }
  160. )
  161. or die "Cannot use CSV: " . Text::CSV->error_diag();
  162. my @columns = qw(
  163. language
  164. file
  165. load_time
  166. time_min
  167. time_max
  168. time_avg
  169. );
  170. # Create the report directory (if needed)
  171. if (not -d $report_dir) {
  172. make_path($report_dir);
  173. }
  174. foreach my $name (keys %{$report_ref}) {
  175. my $csv_file = catfile($report_dir, $name . '.csv');
  176. open my $fh, '>:encoding(UTF-8)', $csv_file;
  177. # Print the CSV columns
  178. $csv->print($fh, \@columns);
  179. while (my ($file, $langs) = each %{$report_ref->{$name}}) {
  180. while (my ($lang, $data) = each %{$langs}) {
  181. # Set the row values
  182. my %row = (
  183. language => $lang,
  184. file => basename($file),
  185. );
  186. my @time_keys = qw(load_time time_min time_max time_avg);
  187. @row{@time_keys} = @{$data}{@time_keys};
  188. # Print the CSV row
  189. $csv->print($fh, [@row{@columns}]);
  190. }
  191. }
  192. close $fh; # close the report
  193. }
  194. return 1;
  195. }
  196. sub start_test {
  197. my ($languages_dir, $executors, $compile_bool) = @_;
  198. my %report;
  199. my %entries = get_dir_entries($languages_dir);
  200. my %files = map_files_to_dirs(%entries);
  201. my %tmpcache;
  202. my $tmpdir = tempdir(CLEANUP => 1);
  203. foreach my $name (sort keys %files) {
  204. # Run only a specific test name
  205. if ($test_name ne '') {
  206. basename($test_name) eq $name or next;
  207. }
  208. printf("\n\t=>> Running test: %s\n", $name);
  209. foreach my $i (0 .. $#{$executors}) {
  210. my $executor = $executors->[$i];
  211. my $lang = $executor->{name};
  212. printf("\n[%s of %s] Testing language: %s\n", $i + 1, $#{$executors} + 1, $lang);
  213. my @args = get_arguments($files{$name});
  214. my %ignore = get_ignores($files{$name});
  215. my @files = files_by_ext($executor->{ext}, $files{$name});
  216. if (@files == 0) {
  217. warn sprintf(" `-> no file has been found with the extension%s: %s\n",
  218. @{$executor->{ext}} > 1 ? 's' : '', join(', ', @{$executor->{ext}}));
  219. next;
  220. }
  221. foreach my $input_file (@files) {
  222. my $basename = basename($input_file);
  223. if (exists $ignore{$basename}) {
  224. printf(" `-> ignoring file: %s\n", $basename);
  225. next;
  226. }
  227. my @run_cmd;
  228. my $temp_file;
  229. my $load_time = 0;
  230. # Case for compiled languages
  231. if ($compile_bool) {
  232. printf(" `-> compilling file: %s\n", $input_file);
  233. my $output_file = mktemp(catfile(tmpdir, 'XXXXXXXX'));
  234. my @cmd = create_cmd($executor->{cmd}, $input_file, $output_file);
  235. # Compile the program
  236. execute_cmd(@cmd)
  237. || do {
  238. warn "[!] Error ($!) in executing the command: @cmd\n";
  239. next;
  240. };
  241. if (not -x $output_file) {
  242. warn "[!] The output file ($output_file) is not executable!\n";
  243. next;
  244. }
  245. $temp_file = $output_file;
  246. push @run_cmd, $output_file;
  247. }
  248. # Case for interpreted languages
  249. else {
  250. # Test the load-time of the interpreter by executing an empty program
  251. if ($test_loadtime) {
  252. my $tmpfile = $tmpcache{$executor->{ext}[0]} // do {
  253. my (undef, $file) = tempfile(DIR => $tmpdir, SUFFIX => ".$executor->{ext}[0]");
  254. $tmpcache{$executor->{ext}[0]} = $file;
  255. $file;
  256. };
  257. my @cmd = create_cmd($executor->{cmd}, $tmpfile);
  258. my $time = time_cmd(@cmd);
  259. if ($time > 0) {
  260. $load_time = time_cmd(@cmd); # time again to get a more accurate result
  261. }
  262. else {
  263. warn "[!] An error occurred while timing the loading time: @cmd";
  264. }
  265. }
  266. push @run_cmd, create_cmd($executor->{cmd}, $input_file);
  267. }
  268. printf(" `-> testing %d times: %s\n", $repeat_n, basename($input_file));
  269. # The array used to store the elapsed times
  270. my @times;
  271. # Run the test N times and store the elapsed times
  272. foreach my $i (1 .. $repeat_n) {
  273. my $elapsed_time = time_cmd(@run_cmd, @args);
  274. if ($elapsed_time == -1) {
  275. warn "[!] An error occurred while executing the command: @run_cmd\n";
  276. last;
  277. }
  278. push @times, ($test_loadtime ? ($elapsed_time - $load_time) : $elapsed_time);
  279. }
  280. # Delete the compiled file
  281. if ($compile_bool and defined $temp_file) {
  282. unlink($temp_file);
  283. }
  284. # Store the collected data
  285. if (@times > 0) {
  286. my $report_name = join(' ', $name, map { s{/}{%}r } @args);
  287. @{$report{$report_name}{$input_file}{$lang}}{qw(load_time time_min time_max time_avg)} =
  288. ($load_time, mMavg(@times));
  289. }
  290. else {
  291. warn "[!] No test has been timed! Skipping file...\n";
  292. next;
  293. }
  294. }
  295. }
  296. }
  297. return %report;
  298. }
  299. #
  300. ## Test the compiled languages
  301. #
  302. if ($test_compiled) {
  303. my %report = start_test($compiled_langs_dir, $compilers, 1);
  304. if (%report) {
  305. print "\n** Generating the reports for compiled languages...\n";
  306. write_report(\%report, $compiled_reports_dir);
  307. print "** Done!\n";
  308. }
  309. else {
  310. warn "\n** No report has been generated for compiled languages!\n";
  311. }
  312. }
  313. #
  314. ## Test the interpreted languages
  315. #
  316. if ($test_interpreted) {
  317. my %report = start_test($interpreted_langs_dir, $interpreters, 0);
  318. if (%report) {
  319. print "\n** Generating the reports for interpreted languages...\n";
  320. write_report(\%report, $interpreted_reports_dir);
  321. print "** Done!\n";
  322. }
  323. else {
  324. warn "\n** No report has been generated for interpreted languages!\n";
  325. }
  326. }
  327. if (not $test_compiled and not $test_interpreted) {
  328. help();
  329. }