【问题标题】:Why is this OCaml program faster than my C program?为什么这个 OCaml 程序比我的 C 程序快?
【发布时间】:2009-10-19 00:17:23
【问题描述】:

我用 C、Python 和 OCaml 编写了一个基本的 Hippity Hop 程序。当然,这可能不是这三种语言的一个很好的基准。但是我得到的结果是这样的:

  • Python:0.350 秒
  • C:0.050 秒
  • 解释 OCaml:0.040 秒
  • 编译的 OCaml:.010

python 的性能并没有让我感到惊讶,但我对 OCaml 的速度(尤其是解释版本)感到相当震惊。为了比较,我将发布 C 版本和 OCaml 版本。

C

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

long get_count(char *name);

int main(int argc, char *argv[])
{
  if (argc != 2){
    printf("Filename must be specified as a positional argument.\n");
    exit(EXIT_FAILURE);
  }

  long count_no = get_count(argv[1]);

  int i;
  for (i = 1; i <= count_no; i++){
    if (((i % 3) == 0) && ((i % 5) == 0)){
      printf("Hop\n");
      continue;
    }
    if ((i % 3) == 0){
      printf("Hoppity\n");
    }
    if ((i % 5) == 0){
      printf("Hophop\n");
    }
  }
  return 0;
}

long get_count(char *name){
  FILE *fileptr = fopen(name, "r");
  if (!fileptr){
    printf("Unable to open file %s.\n", name);
    exit(EXIT_FAILURE);
  }
  size_t text_len = 20;
  char *file_text = calloc(text_len, sizeof(char));
  while (!feof(fileptr)){
    fread(file_text, sizeof(char), text_len, fileptr);
    assert(!ferror(fileptr));
    text_len += 20;
    file_text = realloc(file_text, text_len * sizeof(char));
  }
  long file_as_int = strtol(file_text, NULL, 10);

  free(file_text);
  return file_as_int;
}

OCaml

open String;;

let trim str =
  if str = "" then "" else
  let search_pos init p next =
    let rec search i =
      if p i then raise(Failure "empty") else
      match str.[i] with
      | ' ' | '\n' | '\r' | '\t' -> search (next i)
      | _ -> i
    in
    search init
  in
  let len = String.length str in
  try
    let left = search_pos 0 (fun i -> i >= len) (succ)
    and right = search_pos (len - 1) (fun i -> i < 0) (pred)
    in
    String.sub str left (right - left + 1)
  with
  | Failure "empty" -> ""
;;

let rec iterate_over_numbers curr_num max_num =
  (
   if curr_num <= max_num then (
     if ((curr_num mod 3) == 0) && ((curr_num mod 5) == 0) then 
       print_endline "Hop"
     else if (curr_num mod 3) == 0 then 
       print_endline "Hoppity"
     else if (curr_num mod 5) == 0 then
       print_endline "Hophop";
     iterate_over_numbers (curr_num + 1) max_num
   ))
;;


let fname = Sys.argv.(1);;
let infile = open_in fname;;
let file_text = trim (input_line infile);;
close_in infile;;
let input_number = int_of_string file_text;;
iterate_over_numbers 1 input_number;;

但我很想知道为什么我会得到这些结果。我是在我的 C 程序中做一些愚蠢的事情,还是这只是 OCaml 更快的事情?在我看来有点奇怪,解释程序的运行速度比 C 版本快一点,而编译程序的运行速度是 C 的 5 倍。

【问题讨论】:

  • C 版本使用了哪些编译器开关?
  • @Jonathan - 我刚刚做了一个gcc -o hoppity main.c。老实说,我没有考虑过设置优化级别。 :-)
  • 另外,FWIW,您通过在使用“fopen()”的函数中不使用“fclose()”来泄漏打开的文件。当然,修复它会减慢速度,但是你打开的东西,你应该关闭;你分配的,你应该释放。
  • 这(可能)与您的问题并不真正相关,但while (!feof(fileptr)){ 几乎是一个可以保证的错误。
  • 请注意,这是 FizzBu​​zz 问题的一个次要变体:stackoverflow.com/questions/437/…。但是,它不是重复的-这个问题是在询问时间;另一个是询问实现。

标签: c performance ocaml


【解决方案1】:

您的 C 代码不等同于 OCaml 代码 - 您在 OCaml 中使用了“else if”以避免大量重新计算模数。

“读取长整数”中有大量代码。为什么不直接使用fscanf();它会自动跳过空格和所有这些,并避免您执行malloc() 等。我不经常推荐使用fscanf(),但这看起来像是一个设置 - 单行,两边可能有空格,不好笑东西。


好奇心害死猫 - 但在这种情况下,不是豹子。

我为 MacOS X Intel 下载了 OCaml 3.11.1 并将 OCaml 代码从问题复制到 xxx.ml (OCaml),并将其编译到目标文件 xxx 中(使用“ocamlc -o xxx xxx.ml”);我将 C 代码逐字复制到 yyy.c 中,并使用 fscanf()fclose() 创建了一个变体 zzz.c,并使用“gcc -O -o yyy yyy.c”和“gcc -O -o zzz zzz”编译它们。C”。我创建了一个文件'file3',其中包含:“987654”加上一个换行符。如图所示,我创建了一个 shell 脚本 runthem.sh。请注意,'time' 是一个讨厌的命令,它认为它的输出必须发送到 stderr,即使您不希望它不发送 - 您必须非常努力地工作才能将输出发送到您想要的位置。 (范围命令生成给定范围内的数字,包括在内 - 因此每个程序有 11 个值。)

Osiris JL: cat runthem.sh
for prog in "ocaml xxx.ml" ./xxx ./yyy ./zzz
do
    for iter in $(range 0 10)
    do
        r=$(sh -c "time $prog file3 >/dev/null" 2>&1)
        echo $prog: $r
    done
done
Osiris JL: 

我在运行 Leopard (10.5.8) 的现代 MacBook Pro(3 GHz Core 2 Duo 等,4GB RAM)上运行所有这些。我得到的时间显示为:

Osiris JL: sh runthem.sh
ocaml xxx.ml: real 0m0.961s user 0m0.524s sys 0m0.432s
ocaml xxx.ml: real 0m0.953s user 0m0.516s sys 0m0.430s
ocaml xxx.ml: real 0m0.959s user 0m0.517s sys 0m0.431s
ocaml xxx.ml: real 0m0.951s user 0m0.517s sys 0m0.430s
ocaml xxx.ml: real 0m0.952s user 0m0.516s sys 0m0.431s
ocaml xxx.ml: real 0m0.952s user 0m0.514s sys 0m0.431s
ocaml xxx.ml: real 0m0.951s user 0m0.515s sys 0m0.431s
ocaml xxx.ml: real 0m0.959s user 0m0.515s sys 0m0.431s
ocaml xxx.ml: real 0m0.950s user 0m0.515s sys 0m0.431s
ocaml xxx.ml: real 0m0.956s user 0m0.516s sys 0m0.431s
ocaml xxx.ml: real 0m0.952s user 0m0.514s sys 0m0.432s
./xxx: real 0m0.928s user 0m0.494s sys 0m0.430s
./xxx: real 0m0.938s user 0m0.494s sys 0m0.430s
./xxx: real 0m0.927s user 0m0.494s sys 0m0.430s
./xxx: real 0m0.928s user 0m0.492s sys 0m0.430s
./xxx: real 0m0.928s user 0m0.493s sys 0m0.430s
./xxx: real 0m0.927s user 0m0.493s sys 0m0.430s
./xxx: real 0m0.928s user 0m0.492s sys 0m0.430s
./xxx: real 0m0.933s user 0m0.497s sys 0m0.428s
./xxx: real 0m0.926s user 0m0.494s sys 0m0.429s
./xxx: real 0m0.921s user 0m0.492s sys 0m0.428s
./xxx: real 0m0.925s user 0m0.494s sys 0m0.428s
./yyy: real 0m0.027s user 0m0.026s sys 0m0.001s
./yyy: real 0m0.031s user 0m0.026s sys 0m0.002s
./yyy: real 0m0.028s user 0m0.026s sys 0m0.001s
./yyy: real 0m0.029s user 0m0.026s sys 0m0.002s
./yyy: real 0m0.028s user 0m0.026s sys 0m0.001s
./yyy: real 0m0.029s user 0m0.026s sys 0m0.002s
./yyy: real 0m0.028s user 0m0.026s sys 0m0.001s
./yyy: real 0m0.031s user 0m0.026s sys 0m0.002s
./yyy: real 0m0.028s user 0m0.026s sys 0m0.001s
./yyy: real 0m0.030s user 0m0.026s sys 0m0.002s
./yyy: real 0m0.028s user 0m0.026s sys 0m0.001s
./zzz: real 0m0.030s user 0m0.027s sys 0m0.002s
./zzz: real 0m0.029s user 0m0.027s sys 0m0.001s
./zzz: real 0m0.030s user 0m0.027s sys 0m0.002s
./zzz: real 0m0.029s user 0m0.027s sys 0m0.001s
./zzz: real 0m0.030s user 0m0.027s sys 0m0.002s
./zzz: real 0m0.029s user 0m0.027s sys 0m0.001s
./zzz: real 0m0.030s user 0m0.027s sys 0m0.002s
./zzz: real 0m0.029s user 0m0.027s sys 0m0.001s
./zzz: real 0m0.030s user 0m0.027s sys 0m0.002s
./zzz: real 0m0.029s user 0m0.027s sys 0m0.001s
./zzz: real 0m0.030s user 0m0.027s sys 0m0.002s
Osiris JL:

我没有看到 OCaml 代码比 C 代码运行得更快。我在读取的文件中用较小的数字运行测试,结果同样有利于 C 代码:

站号:345

ocaml xxx.ml: real 0m0.027s user 0m0.020s sys 0m0.005s
ocaml xxx.ml: real 0m0.021s user 0m0.016s sys 0m0.005s
ocaml xxx.ml: real 0m0.025s user 0m0.016s sys 0m0.004s
ocaml xxx.ml: real 0m0.020s user 0m0.015s sys 0m0.003s
ocaml xxx.ml: real 0m0.022s user 0m0.016s sys 0m0.004s
ocaml xxx.ml: real 0m0.019s user 0m0.015s sys 0m0.003s
ocaml xxx.ml: real 0m0.021s user 0m0.016s sys 0m0.004s
ocaml xxx.ml: real 0m0.020s user 0m0.015s sys 0m0.004s
ocaml xxx.ml: real 0m0.021s user 0m0.016s sys 0m0.004s
ocaml xxx.ml: real 0m0.020s user 0m0.015s sys 0m0.004s
ocaml xxx.ml: real 0m0.021s user 0m0.016s sys 0m0.004s
./xxx: real 0m0.003s user 0m0.001s sys 0m0.002s
./xxx: real 0m0.003s user 0m0.001s sys 0m0.001s
./xxx: real 0m0.003s user 0m0.001s sys 0m0.001s
./xxx: real 0m0.002s user 0m0.001s sys 0m0.001s
./xxx: real 0m0.003s user 0m0.001s sys 0m0.001s
./xxx: real 0m0.005s user 0m0.001s sys 0m0.002s
./xxx: real 0m0.003s user 0m0.001s sys 0m0.002s
./xxx: real 0m0.003s user 0m0.001s sys 0m0.001s
./xxx: real 0m0.003s user 0m0.001s sys 0m0.001s
./xxx: real 0m0.003s user 0m0.001s sys 0m0.001s
./xxx: real 0m0.003s user 0m0.001s sys 0m0.001s
./yyy: real 0m0.002s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.003s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.002s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.002s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.002s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.002s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.002s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.001s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.002s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.002s user 0m0.000s sys 0m0.001s
./yyy: real 0m0.003s user 0m0.000s sys 0m0.002s
./zzz: real 0m0.002s user 0m0.000s sys 0m0.001s
./zzz: real 0m0.002s user 0m0.000s sys 0m0.001s
./zzz: real 0m0.002s user 0m0.000s sys 0m0.001s
./zzz: real 0m0.002s user 0m0.000s sys 0m0.001s
./zzz: real 0m0.002s user 0m0.000s sys 0m0.001s
./zzz: real 0m0.002s user 0m0.000s sys 0m0.001s
./zzz: real 0m0.001s user 0m0.000s sys 0m0.001s
./zzz: real 0m0.002s user 0m0.000s sys 0m0.001s
./zzz: real 0m0.003s user 0m0.000s sys 0m0.002s
./zzz: real 0m0.002s user 0m0.000s sys 0m0.001s
./zzz: real 0m0.002s user 0m0.000s sys 0m0.001s

站号:87654

ocaml xxx.ml: real 0m0.102s user 0m0.059s sys 0m0.041s
ocaml xxx.ml: real 0m0.102s user 0m0.059s sys 0m0.040s
ocaml xxx.ml: real 0m0.101s user 0m0.060s sys 0m0.040s
ocaml xxx.ml: real 0m0.103s user 0m0.059s sys 0m0.041s
ocaml xxx.ml: real 0m0.102s user 0m0.059s sys 0m0.041s
ocaml xxx.ml: real 0m0.101s user 0m0.059s sys 0m0.041s
ocaml xxx.ml: real 0m0.102s user 0m0.059s sys 0m0.040s
ocaml xxx.ml: real 0m0.103s user 0m0.059s sys 0m0.040s
ocaml xxx.ml: real 0m0.101s user 0m0.059s sys 0m0.040s
ocaml xxx.ml: real 0m0.102s user 0m0.059s sys 0m0.040s
ocaml xxx.ml: real 0m0.105s user 0m0.059s sys 0m0.041s
./xxx: real 0m0.092s user 0m0.044s sys 0m0.038s
./xxx: real 0m0.087s user 0m0.044s sys 0m0.039s
./xxx: real 0m0.085s user 0m0.044s sys 0m0.038s
./xxx: real 0m0.084s user 0m0.044s sys 0m0.038s
./xxx: real 0m0.085s user 0m0.044s sys 0m0.039s
./xxx: real 0m0.086s user 0m0.045s sys 0m0.039s
./xxx: real 0m0.085s user 0m0.044s sys 0m0.039s
./xxx: real 0m0.085s user 0m0.044s sys 0m0.038s
./xxx: real 0m0.084s user 0m0.044s sys 0m0.038s
./xxx: real 0m0.084s user 0m0.044s sys 0m0.039s
./xxx: real 0m0.083s user 0m0.044s sys 0m0.038s
./yyy: real 0m0.004s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.004s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.004s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.004s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.005s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.005s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.004s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.004s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.004s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.004s user 0m0.003s sys 0m0.001s
./yyy: real 0m0.006s user 0m0.003s sys 0m0.002s
./zzz: real 0m0.004s user 0m0.003s sys 0m0.001s
./zzz: real 0m0.004s user 0m0.003s sys 0m0.001s
./zzz: real 0m0.004s user 0m0.003s sys 0m0.001s
./zzz: real 0m0.004s user 0m0.003s sys 0m0.001s
./zzz: real 0m0.004s user 0m0.003s sys 0m0.001s
./zzz: real 0m0.005s user 0m0.003s sys 0m0.002s
./zzz: real 0m0.004s user 0m0.003s sys 0m0.001s
./zzz: real 0m0.004s user 0m0.003s sys 0m0.001s
./zzz: real 0m0.004s user 0m0.003s sys 0m0.001s
./zzz: real 0m0.004s user 0m0.003s sys 0m0.001s
./zzz: real 0m0.005s user 0m0.003s sys 0m0.001s

显然,YMMV - 但 OCaml 似乎比 C 慢了相当大的幅度,但如果给定文件中的数字足够小,那么启动和文件读取将主导进程时间。

C 的时序,尤其是在较小的数字时,是如此之快以至于它们并不是那么可靠。

【讨论】:

  • 仅供参考,这些似乎都没有产生巨大的性能差异。尽管使用 fscanf 确实使代码更具可读性!
  • 有趣:正如我在 cmets 中问的主要问题 - 给你的数字有多大?如果你迭代 N 次会发生什么(其中 N 是一个足够大的数字,可以让最快的程序花费几秒钟的时间)?不要忘记,如果您进行 10,000 次迭代,您将需要关闭该文件。
【解决方案2】:

0.05 以下的时间可能是一个简单的噪音。将主程序重复足够多的时间以在 C 中实际获得约 1 秒的执行时间。(我的意思是在程序本身的循环中重复它,而不是再次运行它)

您是否在启用优化的情况下编译了代码?您是否尝试减少分支机构的数量? (和比较)

if (i % 3 == 0) {
  if (i % 5 == 0) {
    printf("Hop\n");
    continue;
  }
  printf("Hoppity\n");
} else if (i % 5 == 0){
  printf("Hophop\n");
}

您是否尝试查看汇编器输出?

printf 也很慢。请改用puts("Hop"),因为无论如何您都不使用格式。

【讨论】:

  • 这是可能的,但您必须对其进行测试。您在每 15 次尝试中节省的时间(您只进行 1 次检查)可能无法平衡您在进行 3 次检查的数字上损失的时间。只有测试才能知道:)(评论了检查 i%15、i%3、i%5 的建议 - 后来被删除了)
  • 是的,因为代码有错别字,我删除了它,然后意识到它可能不会更有效,所以我把它删除了。
  • 到目前为止,如果我提高优化级别,它似乎会快 很多
  • +1 ,除非使用饮食库中非常基本的 printf() 子系统,否则 printf() 确实会减慢速度,尤其是在 glibc 生物舒适度方面。
  • 如果您使用“else”来打印“Hoppity”,则不需要“继续”。
【解决方案3】:

在像这样的小程序中,通常很难猜测为什么事情会以这种方式运行。我想如果我这样做,我会写这样的代码(暂时忽略错误检查):

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

int main(int argc, char **argv) { 
    static char buffer[20];   
    int limit, i;

    freopen(argv[1], "r", stdin);
 fgets(buffer, sizeof(buffer), stdin);
    limit = atoi(buffer);

    for (i=1; i<=limit; i++) {
        int div3=i%3==0;
        int div5=i%5==0;
        if (div3 && div5) 
            puts("Hop");
        else if (div3)
            puts("Hoppity");
        else if (div5)
            puts("HopHop");
    }
    return 0;
}

使用freopen 可以避免创建另一个文件流,而只是将标准输入连接到指定文件。不能保证它会更快,但无论如何也不可能更慢。

同样,一个好的编译器可能会注意到i 在整个循环体中是恒定的,并且将两个余数运算分解,因此它只执行一次。这里我是手动完成的,可能不会更快,但几乎肯定不会更慢。

使用puts 代替printf 非常相似——它可能不会更快,但几乎肯定不会更慢。以您的方式使用printf,它必须扫描整个字符串以查找“%”,以防您要求转换,但由于puts 不进行任何转换,因此它不会必须这样做。

对于这样一个很小的程序,还有另一个可能更重要的因素:puts 通常会比printf 小得多。你没有说你是怎么做计时的,但是如果它包括加载代码的时间,那么非常小的代码很可能比执行时间产生更大的差异。

【讨论】:

    【解决方案4】:

    任何主要涉及打开文件和读取文件的程序都受到打开文件和读取文件的速度的限制。您在此处执行的 C 计算将花费打开文件和读取文件的时间的百万分之一到千分之一。

    我认为这个网站很有用:http://norvig.com/21-days.html#answers

    【讨论】:

    • 如果这是真的,C 和 OCaml 之间就不会有这样的差异(因为两者都受磁盘 IO 的约束)。 OP 表示,当他提高优化级别时,C 代码变得更快,所以我怀疑这可能是问题所在。
    • 但是访问文件所花费的时间是完全随机的。如果文件仍然被缓存,则它所花费的时间比程序冷启动时要少得多。如果他先运行 OCaml,然后运行 ​​C,则 C 可能会获胜。如果他运行 C 程序超过一百万次,则平均时间会显着下降。
    • @Kinopiko - 我实际上是针对同一文件的副本运行这些。
    【解决方案5】:

    我很想知道在 get_count() 中花费了多少时间。

    我不确定这有多重要,但您正在读取一个长字符串,这意味着该字符串不能大于 20 个字节或 10 个字节(2^64 = 大约 20 个字符长十进制数,或 2^32 = 一些 10 个字符长的十进制数),因此您不需要在 get_count 中使用 while 循环。此外,您可以在堆栈上分配 file_text,而不是调用 calloc - 但我想您仍然需要将其归零,或者找到长度并将最后一个字节设置为 null。

    file_length = lseek(fileptr, 0, SEEK_END);
    

    【讨论】:

    • 有趣的地方。我想一遍又一遍地重新分配内存效率会很低。我想我只需要熟悉 C 分析器。 :-)
    • 对局部变量使用上面 Jonathan 的 fscanf 建议,看起来并没有太大的不同。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-09-09
    • 1970-01-01
    • 1970-01-01
    • 2011-07-10
    • 1970-01-01
    • 2018-01-21
    • 1970-01-01
    相关资源
    最近更新 更多