2つのコードは非常に異なることをします。
import Data.Bits
main = do
print $ length $ filter (\i -> i .&. (shift 1 (i `mod` 4)) /= 0) [0..123456789]
123456790のリストをInteger
(怠惰に)作成し、それぞれの剰余を4を法として取ります(最初に、Integer
が生のマシンの整数をラップするのに十分小さいかどうかのチェックを含み、次に除算の後に符号チェックを行います。これは、mod
非負の結果のみを返すためです- ghc-7.6.1にはそのためのプリモップがあるので、mod
以前ほど使用するブレーキではありません)、1を適切なビット数だけ左にシフトしInteger
ます。これには、「ビッグ」への変換が含まれますInteger
。 GMPへの呼び出しは、ビット単位で、i
さらにGMPへの呼び出しを行い、結果が0であるかどうかをチェックします。これにより、GMPへの別の呼び出しまたは小整数への変換が発生しますが、GHCがここで何を行うかはわかりません。次に、結果がゼロ以外の場合、新しいリストセルが作成されます。Integer
入れられ、によって消費されlength
ます。これは多くの作業が行われ、不特定の数値タイプがデフォルトでに設定されているため、そのほとんどが不必要に複雑になっていInteger
ます。
Cコード
#include <stdio.h>
int main(void) {
int count = 0;
const int max = 123456789;
int i;
for (i = 0; i < max; ++i)
if ((i & (1 << i % 4)) != 0)
++count;
printf("count: %d\n", count);
return 0;
}
(私はのリターンタイプを修正する自由を取りましたmain
)、はるかに少ないです。それはを取り、int
それを別のものと比較し、小さい場合はビット単位で、最初のint
3 (1)int
を取り、1を適切なビット数で左にシフトし、ビット単位でそれと最初のを取り、int
ゼロ以外の場合別int
のをインクリメントし、次に最初をインクリメントします。これらはすべてマシン操作であり、生のマシンタイプで動作します。
そのコードをHaskellに翻訳すると、
module Main (main) where
import Data.Bits
maxNum :: Int
maxNum = 123456789
loop :: Int -> Int -> Int
loop acc i
| i < maxNum = loop (if i .&. (1 `shiftL` (i .&. 3)) /= 0 then acc + 1 else acc) (i+1)
| otherwise = acc
main :: IO ()
main = print $ loop 0 0
より近い結果が得られます。
C, gcc -O3:
count: 30864196
real 0m0.180s
user 0m0.178s
sys 0m0.001s
Haskell, ghc -O2:
30864196
real 0m0.247s
user 0m0.243s
sys 0m0.003s
Haskell, ghc -O2 -fllvm:
30864196
real 0m0.144s
user 0m0.140s
sys 0m0.003s
GHCのネイティブコードジェネレーターは特に優れたループオプティマイザーではないため、llvmバックエンドを使用すると、ここで大きな違いが生じますが、ネイティブコードジェネレーターでさえそれほど悪くはありません。
さて、私はモジュラス計算をビット単位の2の累乗モジュラスに置き換える最適化を行いましたが、GHCのネイティブコードジェネレーターは(まだ)それを行いません。したがって、 ``` rem 4`` instead of
。&。3`、ネイティブコードジェネレーターは(ここでは)実行に1.42秒かかるコードを生成しますが、llvmバックエンドはその最適化を行い、手作りの最適化と同じコードを生成します。
それでは、 gsprの質問に移りましょう。
LLVMは元のコードに大きな影響を与えませんでしたが、実際には変更されたコードに影響を及ぼしました(理由を知りたいです...)。
さて、使用された元のコードInteger
砂リストでは、llvmはこれらをどう処理するかをよく知らないため、そのコードをループに変換することはできません。変更されたコードはInt
sを使用し、vector
パッケージはコードをループに書き換えるので、llvmはそれを適切に最適化する方法を知っています。
(1)通常のバイナリコンピュータを想定しています。div
この最適化は、命令がシフトよりも高速である非常にまれなプラットフォームを除いて、最適化フラグがなくても通常のCコンパイラによって実行されます。