なぜemacsの乗算性能が悪いのですか?

私はemacsとCの2つのバージョンのループを比較しました。 Emacsのバイトコンパイルバージョンは4.5秒、Cバージョンは0.22秒です。なぜそんなに遅い?どのように最適化する?

Emacsのバージョン:

;; -*- lexical-binding: t -*-

(defun test-me ()
  (let ((i 0) (j 0) (t1 (float-time)))
    (while (< i 100000000)
      (setq j (* i 7))
      (setq i (+ i 1)))
    (- (float-time) t1)))

Cバージョン:

#include 
#include 
#include 

int main(int argc, char **argv) {
    int i = 0;
    int j = 0;

    clock_t start = clock();
    while(i < 100000000) {
        j = i * 7;
        i = i + 1;
    }

    clock_t end = clock();
    float seconds = (float)(end - start)/CLOCKS_PER_SEC;
    printf("%f\n", seconds);

    return 0;
}
2
あなたが最適化技術に興味があるなら、例えば、合計の合計を加算するなどの方法はありません。一般に大きな数値を追加する方がコストが高いので、summandをsummandに加算するよりも良い方法です。追加される数字の詳細を知っている場合は、シフトや乗算などで置き換えることができるように、通常はもっと多くのトリックを使用することもできます
追加された 著者 Yann Trevin,
PS。速度の差は乗算自体ではなく、制御構造によって引き起こされる可能性があります。 AFAIK、Pythonは算術演算に非常に最適化されたlong-mathライブラリを使用しているので、この点では素朴なCよりも遅くなる可能性は低いです。
追加された 著者 Yann Trevin,
ガベージコレクションが行われましたか?それは物事を遅くすることができます。また、Emacsの整数が限られているため、これらの計算の結果はおそらく間違っていることに注意してください
追加された 著者 Andrew Rimmer,
私は実際にElispがこのテストでCの20倍の遅さしかないことに感心しています。
追加された 著者 sds,
私はelispが速度のためにコンパイルされたCにアプローチするとは思わないでしょう。ここでの速度の差は、PythonやPerlとCの間で予想される速度に匹敵します。速度を上げるためのトリックがあるかもしれませんが、数値処理のループではこれよりずっと良いとは思いません。
追加された 著者 Eric Rogers,
面白いことに、Pythonの同等の機能はelispのバージョンよりも遅いです。しかし、Rubyの同等物は、より良い処理がより速い。
追加された 著者 ICodeForCoffee,
これは、Emacsが提供する最もパフォーマンス重視の関数がelispではなくC言語で書かれている理由です。
追加された 著者 Mark Ireland,

2 答え

Emacs Lispはネイティブにコンパイルされた言語ではなく、解釈可能な移植可能なバイトコードにコンパイルされます。さらに、Emacsのバイトコードはこれまでに設計された最速のバイトコードではありません。特に、字句変数へのアクセスはおそらくそれほど高速ではなく、コードはそれらに大きく依存しています。 Stefanが述べたように、Emacs Lispでのコードの方がCでのコードのほうが20倍も遅いというのは驚くべきことです。

歴史的に、バイトコードをさらに最適化するインセンティブはほとんどありませんでした。なぜなら、Emacsは主にテキストの編集に使用され、Emacsバッファは高度に最適化されていたからです。今日、多くの人がGnus、Eww、WanderlustでEmacsを使用しているので、Emacs Lispランタイムをより良い方法で再実装するのに一年か二か月を費やす主人公のために待っています。

1
追加された

Emacsはコードで何をしていますか?私の分析はここにあります:

ループを実行するelispコードのバイトコード:

0       constant  0
1       dup       
2       constant  float-time
3       call      0
4:1     stack-ref 2
5       constant  100000000
6       lss       
7       goto-if-nil 2
10      stack-ref 2
11      constant  7
12      mult      
13      stack-set 2
15      stack-ref 2
16      add1      
17      stack-set 3
19      goto      1
22:2    constant  float-time
23      call      0
24      stack-ref 1
25      diff      
26      return  

オペコード "mult"は、big switch文のcase式として定義されています。

CASE (Bmult):
      BEFORE_POTENTIAL_GC ();
      DISCARD (1);
      TOP = Ftimes (2, &TOP);
      AFTER_POTENTIAL_GC ();
      NEXT;

「Ftimes」は次のように定義されます。

DEFUN ("*", Ftimes, Stimes, 0, MANY, 0,
doc: /* Return product of any number of arguments, which are numbers or markers.
usage: (* &rest NUMBERS-OR-MARKERS)  */)
(ptrdiff_t nargs, Lisp_Object *args)
{
    return arith_driver (Amult, nargs, args);
}

算術ドライバ:

static Lisp_Object
arith_driver (enum arithop code, ptrdiff_t nargs, Lisp_Object *args)
{
  Lisp_Object val;
  ptrdiff_t argnum, ok_args;
  EMACS_INT accum = 0;
  EMACS_INT next, ok_accum;
  bool overflow = 0;

  switch (code)
    {
    case Alogior:
    case Alogxor:
    case Aadd:
    case Asub:
      accum = 0;
      break;
    case Amult:
    case Adiv:
      accum = 1;
      break;
    case Alogand:
      accum = -1;
      break;
    default:
      break;
    }

  for (argnum = 0; argnum < nargs; argnum++)
    {
      if (! overflow)
    {
      ok_args = argnum;
      ok_accum = accum;
    }

      /* Using args[argnum] as argument to CHECK_NUMBER_... */
      val = args[argnum];
      CHECK_NUMBER_OR_FLOAT_COERCE_MARKER (val);

      if (FLOATP (val))
    return float_arith_driver (ok_accum, ok_args, code,
                   nargs, args);
      args[argnum] = val;
      next = XINT (args[argnum]);
      switch (code)
    {
    case Aadd:
      overflow |= INT_ADD_WRAPV (accum, next, &accum);
      break;
    case Asub:
      if (! argnum)
        accum = nargs == 1 ? - next : next;
      else
        overflow |= INT_SUBTRACT_WRAPV (accum, next, &accum);
      break;
    case Amult:
      overflow |= INT_MULTIPLY_WRAPV (accum, next, &accum);
      break;
    case Adiv:
      if (! (argnum || nargs == 1))
        accum = next;
      else
        {
          if (next == 0)
        xsignal0 (Qarith_error);
          if (INT_DIVIDE_OVERFLOW (accum, next))
        overflow = true;
          else
        accum /= next;
        }
      break;
    case Alogand:
      accum &= next;
      break;
    case Alogior:
      accum |= next;
      break;
    case Alogxor:
      accum ^= next;
      break;
    case Amax:
      if (!argnum || next > accum)
        accum = next;
      break;
    case Amin:
      if (!argnum || next < accum)
        accum = next;
      break;
    }
    }

  XSETINT (val, accum);
  return val;
}

INT_MULTIPLY_WRAPVは次のコードで乗算を行います。

/* Return A  B, where the operation is given by OP.  Use the
   unsigned type UT for calculation to avoid overflow problems.
   Convert the result to type T without overflow by subtracting TMIN
   from large values before converting, and adding it afterwards.
   Compilers can optimize all the operations except OP.  */
#define _GL_INT_OP_WRAPV_VIA_UNSIGNED(a, b, op, ut, t, tmin, tmax) \
  (((ut) (a) op (ut) (b)) <= (tmax) \
   ? (t) ((ut) (a) op (ut) (b)) \
   : ((t) (((ut) (a) op (ut) (b)) - (tmin)) + (tmin)))

"mult"が実行されるたびに、1つのx86命令だけを実行する必要があるときには、多くの追加命令でswitch文、ラッパーコードを実行します。提供されたコードでは、Garbage Collectionはまったく実行されませんでした(変数gcs-doneで判断)。

潜在的なボトルネック:数値が浮動小数点かどうかのチェック、オーバーフローのチェック、switch文。

私は、教育目的で GNU Emacs のGPLライセンスコードの一部を使用しました。

1
追加された