Kan-Ru Chen's Weblog

Speed comparison C vs Common Lisp

昨天看到有人轉噗一則 StackOverflow 上面的問題,內容是問他分別用 C, python, erlang 與 haskell 寫了 Project Euler 的第十二題,但是 haskell 版本慢得不像話,該如何改進?

以下是苦主的原始 C 語言版:

    #include <stdio.h>
    #include <math.h>
    int factorCount (long n)
    {
        double square = sqrt (n);
        int isquare = (int) square;
        int count = isquare == square ? -1 : 0;
        long candidate;
        for (candidate = 1; candidate <= isquare; candidate ++)
            if (0 == n % candidate) count += 2;
        return count;
    }
    int main ()
    {
        long triangle = 1;
        int index = 1;
        while (factorCount (triangle) < 1001)
        {
            index ++;
            triangle += index;
        }
        printf ("%ld\n", triangle);
    }

我的執行時間約為 5.48s

    % gcc -lm -o euler12.bin euler12.c
    % time ./euler12.bin 
    842161320
    ./euler12.bin  5.48s user 0.00s system 99% cpu 5.484 total

手癢想看看據說可以編譯出不錯的機器碼的 Common Lisp 速度怎麼樣,於是第 一個版本如下 (用 sbcl 執行):

    (defun factor-count (n)
      (let* ((square (sqrt n))
             (squaref (floor square))
             (count (if (eql squaref square) -1 0)))
          (loop
             for cand from 1 to squaref
             count (eql 0 (rem n cand)) into c
             finally (return (+ count c c)))))
    (defun first-triangle-over (n)
        (loop
           for idx from 1
           sum idx into triangle
           until (>= (factor-count triangle) n)
           finally (return triangle)))
    ;; (time (first-triangle-over 1000))
    ;; Evaluation took:
    ;;   11.192 seconds of real time
    ;;   11.184699 seconds of total run time (11.184699 user, 0.000000 system)
    ;;   99.94% CPU
    ;;   30,135,882,489 processor cycles
    ;;   32,912 bytes consed
    ;;   
    ;; 842161320

還不錯,11.192s,這個版本採用原文中給 haskell 的建議,使用 rem 而不是 mod,可以 加快一點速度。再給一點關於型別的提示,然後把第一個 function inline,第 二版可以跑出和 C 版本差不多的成績 5.563s :-)

    (declaim (inline factor-count)) ;; <----
    (defun factor-count (n)
      (let* ((square (sqrt n))
             (squaref (floor square))
             (count (if (eql squaref square) -1 0)))
          (loop
             for cand from 1 to squaref
             count (eql 0 (rem n cand)) into c
             finally (return (+ count c c)))))
    (defun first-triangle-over (n)
        (loop
           for idx of-type fixnum from 1 ;; <----
           sum idx into triangle of-type fixnum  ;; <----
           until (>= (factor-count triangle) n)
           finally (return triangle)))
    ;; (time (first-triangle-over 1000))
    ;; Evaluation took:
    ;;   5.563 seconds of real time
    ;;   5.556347 seconds of total run time (5.556347 user, 0.000000 system)
    ;;   99.87% CPU
    ;;   14,970,270,903 processor cycles
    ;;   32,768 bytes consed
    ;;
    ;; 842161320

純粹湊個熱鬧而已,有興趣的人可以試試看把 loop 改成和原文其他語言一樣使 用遞迴來實做,大部分 Lisp 都有做 TCO,速度應該差不多...

結論:Common Lisp is awesome ;-)