LLVMとコンパイラと投資
--.--.-- *--

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書く事で広告が消せます。

2012.03.02 *Fri

V8の文字列結合が速いって?

文字列結合のベンチマークをいろんな処理系でやってみた

というスライドが非常に面白いなと思ったので、色々と試していました。



ベンチマークのソースコードは、
http://bit.ly/pNNmGY
で公開されているようです。

疑問に思ったポイントは、javascriptの文字列連結が速すぎることですね。。
buf+=s+s+s 0.066

私の環境で測定してみたところ、0.022でした。
比較して、C++で0.450、Java7で0.350です。

該当のコード
bm.task("buf+=s+s+s", function(loop) {
  var m = members();
  var s1 = m[0], s2 = m[1], s3 = m[2], s4 = m[3], s5 = m[4];
  var out = "";
  while (--loop >= 0) {
    var buf = "";
    buf += "<table>\n";
    for (var j = 0; j < 20; j++) {
      buf += "  <tr>\n\
    <td>" + s1 + "</td>\n\
    <td>" + s2 + "</td>\n\
    <td>" + s3 + "</td>\n\
    <td>" + s4 + "</td>\n\
    <td>" + s5 + "</td>\n\
  </tr>\n";
    }
    buf += "</table>\n";
    out = buf;
    verify(out);
  }
  verify(out);
});

おそらくですが、V8のJITコンパイラであるCrankshaftの最適化(ループ不変式の移動LICM)により、
下記のようになっているのかなと思いました。

bm.task("buf+=s+s+s(test)", function(loop) {
  var m = members();
  var s1 = m[0], s2 = m[1], s3 = m[2], s4 = m[3], s5 = m[4];
  var out = "";
  while (--loop >= 0) {
    var buf = "";
    buf += "<table>\n";
  var buf2 = "";
  buf2 += "  <tr>\n\
    <td>" + s1 + "</td>\n\
    <td>" + s2 + "</td>\n\
    <td>" + s3 + "</td>\n\
    <td>" + s4 + "</td>\n\
    <td>" + s5 + "</td>\n\
  </tr>\n";
    for (var j = 0; j < 20; j++) {
      buf += buf2;
    }
    buf += "</table>\n";
    out = buf;
    verify(out);
  }
  verify(out);
});

上記のソースコードで測定してみると、結果はどちらも0.022くらいと同じになります。

本当に移動しているかどうかは、後ろのほうで確認しました。

でも正直なところ、buf2のところをループの外に追い出せるのはすごく面白いと思っています。

仮に、"buf+=s;buf+=s;buf+=s"
のようなコードだった場合、
buf+=s; <-- bufに対して副作用が存在しているため、引き上げれない。
buf+=s; <-- 上記と同様に引き上げれない。
buf+=s;

たとえば、Javaでも
buf += s + s + s;
のように書いた際に、
buf.append(s).append(s).append(s)
buf = new StringBuffer().append(buf).append(s).append(s).append(s).toString() らしいです。

にjavacの段階で変換されるらしいのですが、
上記のように変換されてしまったら、
s+s+sというimmtableなString型を結合しているというcontextは失われている。
V8のようにくくり出すことができなくなっているのではないでしょうか。
※そもそも私はJavaの演算子の優先順序や式の評価順序を理解していないため、指摘が的外れかも。。

式中のimmtableという情報を最適化で活用し、LICMで引き上げちゃってるあたり、
V8は色々裏で手を回しているのだと思います。

この辺、haskellあたりは得意なのかな?とおもい、
GHCでは"不変な部分"ですという情報を使って高度な最適化とかしちゃってるのでしょうかね。
※という部分に興味を持ってGHC勉強会に突入しようとおもっていたのですが、仕事でドタキャン


上記のように
不変部分をループの外に移動して、
JavaやC++のベンチマークのソースコードを書き換えて測定してみましたが、
※大体0.100〜0.080くらいだったかな。
まだV8のほうが2倍以上速いのはスゴイことだと思いました。
他にも色々差はありそうです。

最近忙しいのでまったく時間取れませんが、下記の2点は確認しておきたいです。
(1)本当にbuf2っぽくループの外に追い出されているのかどうか。
(2)ループの外に追い出すロジックがどんな感じになっているのか。

ループの外に追い出すロジックは昔既に追っていたのですが、
Flagチェックの細かいところまで追っていなかったので、よくわからんですね。
LICMの引き上げ対象や、副作用の有無やチェック方法に注意してもっかい見てみようかなと。

とおもってtrunk版を取得してみたら、
LICMの該当部分が30stepくらいだったのが90stepに膨れ上がってるんですけど、、


---追記
やっぱり引き上げてました。
buf+=s+s+s
にて確認

V8 Crankshaftの中間表現そのままで失礼します。

crankshaftの入り口
  begin_block
    name "B10"
    from_bci -1
    to_bci -1
    predecessors "B9"
    successors "B8"
    xhandlers
    flags "dom-loop-succ"
    dominator "B9"
    loop_depth 2
    begin_states
      begin_locals
        size 0
        method "None"
      end_locals
    end_states
    begin_HIR
      0 0 v158 BlockEntry  <|@
      0 0 v159 Simulate id=155 <|@
      0 0 v160 StackCheck  <|@
      0 3 t161 Constant 0x229190dd <String[15]\:   <tr>\n    <td>> <|@
      0 0 t162 CheckNonSmi t161 <|@
      0 0 t163 CheckInstanceType string t161 <|@
      0 0 t164 CheckNonSmi t141 <|@
      0 0 t165 CheckInstanceType string t141 <|@
      0 3 t166 StringAdd t161 t141 <|@
      0 3 t167 Constant 0x229190f9 <String[14]\: </td>\n    <td>> <|@
      0 0 t168 CheckNonSmi t166 <|@
      0 0 t169 CheckInstanceType string t166 <|@
      0 0 t170 CheckNonSmi t167 <|@
      0 0 t171 CheckInstanceType string t167 <|@
      0 3 t172 StringAdd t166 t167 <|@
      0 0 t173 CheckNonSmi t172 <|@
      0 0 t174 CheckInstanceType string t172 <|@
      0 0 t175 CheckNonSmi t143 <|@
      0 0 t176 CheckInstanceType string t143 <|@


Crankshaftの最適化、canonicalizeの出口
begin_cfg
  name "Canonicalize"
  begin_block
    name "B10"
    from_bci -1
    to_bci -1
    predecessors "B9"
    successors "B8"
    xhandlers
    flags "dom-loop-succ"
    dominator "B9"
    loop_depth 2
    begin_states
      begin_locals
        size 0
        method "None"
      end_locals
    end_states
    begin_HIR
      0 0 v158 BlockEntry  <|@
      0 0 v159 Simulate id=155 <|@
      0 0 v160 StackCheck  <|@
      0 1 t161 Constant 0x229190dd <String[15]\:   <tr>\n    <td>> type[string] <|@
      0 0 t164 CheckNonSmi t92 <|@
      0 0 t165 CheckInstanceType string t92 <|@
      0 1 t166 StringAdd t161 t92 type[string] <|@
      0 1 t167 Constant 0x229190f9 <String[14]\: </td>\n    <td>> type[string] <|@
      0 1 t172 StringAdd t166 t167 type[string] <|@
      0 0 t175 CheckNonSmi t94 <|@
      0 0 t176 CheckInstanceType string t94 <|@
      0 1 t177 StringAdd t172 t94 type[string] <|@
      0 1 t178 Constant 0x229190f9 <String[14]\: </td>\n    <td>> type[string] <|@
      0 1 t183 StringAdd t177 t178 type[string] <|@
      0 0 t186 CheckNonSmi t90 <|@
      0 0 t187 CheckInstanceType string t90 <|@
      0 1 t188 StringAdd t183 t90 type[string] <|@
      0 1 t189 Constant 0x229190f9 <String[14]\: </td>\n    <td>> type[string] <|@
      0 1 t194 StringAdd t188 t189 type[string] <|@
      0 0 t197 CheckNonSmi t96 <|@
      0 0 t198 CheckInstanceType string t96 <|@
      0 1 t199 StringAdd t194 t96 type[string] <|@
      0 1 t200 Constant 0x229190f9 <String[14]\: </td>\n    <td>> type[string] <|@
      0 1 t205 StringAdd t199 t200 type[string] <|@
      0 0 t208 CheckNonSmi t95 <|@
      0 0 t209 CheckInstanceType string t95 <|@
      0 1 t210 StringAdd t205 t95 type[string] <|@
      0 1 t211 Constant 0x22919115 <String[14]\: </td>\n  </tr>\n> type[string] <|@
      0 1 t216 StringAdd t210 t211 type[string] <|@
      0 2 t221 StringAdd t140 t216 type[string] <|@
      0 1 i284 Constant 1 <|@
      0 2 i223 Add i146 i284 ! <|@
      0 0 v224 Simulate id=151 var[5] = t221, var[11] = i223 <|@
      0 0 v225 Goto B8 <|@
    end_HIR
  end_block


Crankshaftの最適化、GVNの出口
※CrankshaftのLICMは、GVNの中で行っているため、LICMで移動しているのは確実

begin_cfg
  name "Global value numbering"
  begin_block
    name "B10"
    from_bci -1
    to_bci -1
    predecessors "B9"
    successors "B8"
    xhandlers
    flags "dom-loop-succ"
    dominator "B9"
    loop_depth 2
    begin_states
      begin_locals
        size 0
        method "None"
      end_locals
    end_states
    begin_HIR
      0 0 v158 BlockEntry  <|@
      0 0 v159 Simulate id=155 <|@
      0 0 v160 StackCheck  <|@
      0 2 t221 StringAdd t140 t216 type[string] <|@
      0 2 i223 Add i146 i284 ! <|@
      0 0 v224 Simulate id=151 var[5] = t221, var[11] = i223 <|@
      0 0 v225 Goto B8 <|@
    end_HIR
  end_block

この段階でも、文字列結合がStringAddなるマクロ命令で表現されている点が特徴なのだと思います。

Java HotSpotでは、文字列結合は、JITコンパイルの入り口で、
もっと粒度の小さいメモリアクセス命令や加減算命令との組み合わせに細分化されてしまったはず。
文字列結合です、といったcontextを活かしにくい構造だったかな???
※粒度の小さい命令に細分化したぶん、汎用的な最適化に掛かりやすくなりますが。

V8では、最終的にStringAddマクロのままコード生成までいき、
StringAddStub生成でJITアセンブリします。
StringAddStubのJITアセンブリは、各アーキテクチャ(x86/x64/arm/mips)各々で実装されていました。たしか。

2012.01.28 *Sat

gdb python scriptingの機能を使ってllvmをデバッグする

最近gdb7.4がリリースされ、python scripting機能が強化されたとアナウンスされてました。

python scriptingは細々と使っていたのですが、
llvm(に限らず)のデバッグで有効活用出来そうなサンプルを作成出来たので紹介します。

サンプルのpythonコードです。
# -*- coding: utf-8 -*-
# coding:utf-8
#import gdb
import re
import sys
def print_value(value):
  frame = gdb.selected_frame()
  val = gdb.Frame.read_var(frame, value)
  print val
def eval_value(value):
  gdb.parse_and_eval(value)
def breakpoint_stop_handler (event):
  if (isinstance (event, gdb.StopEvent)):
    print "event type: stop"
  if (isinstance (event, gdb.BreakpointEvent)):
    print "event type: break"
    print "breakpoint number: %s" % (event.breakpoint.number)
#    eval_value("F.getName()")
    eval_value("BB->dump()")
    get_name = False
    f = open('./t', 'r')
    for line in f:
#      print "search" + line
      if re.search("ray_sphere_intersect", line):
        get_name = True
    if (get_name):
      print "break ..."
    else:
      print "not match"
      gdb.execute("continue")
class test_events (gdb.Command):
  def __init__ (self):
    gdb.Command.__init__ (self, "test_events", gdb.COMMAND_STACK)
  def invoke (self, arg, from_tty):
    gdb.events.stop.connect (breakpoint_stop_handler)
gdb.events.stop.connect (breakpoint_stop_handler)


上記は限定した条件下で動作するスクリプトになっていて、
使い方は以下のようになります。


elise@elise-desktop:~/project/python/gdb$ gdb $bin/opt
GNU gdb (GDB) 7.3
Reading symbols from /home/elise/language/llvm/trunk/build/Debug+Asserts/bin/opt...done.
(gdb) break GVN.cpp:2153
Breakpoint 1 at 0x83328d1: file /home/elise/language/llvm/trunk/lib/Transforms/Scalar/GVN.cpp, line 2148.
(gdb) tty t
(gdb) source pscript001.py 
(gdb) run -o t.bc -f -std-compile-opts float.bc <-- aobenchです
Breakpoint 1, (anonymous namespace)::GVN::runOnFunction (this=0x8a39910, F=...)
  at /home/elise/language/llvm/trunk/lib/Transforms/Scalar/GVN.cpp:2154
  2154      bool removedBlock = MergeBlockIntoPredecessor(BB, this);
  event type: stop
  event type: break
  breakpoint number: 1
  not match
Breakpoint 1, (anonymous namespace)::GVN::runOnFunction (this=0x8a39910, F=...)
  at /home/elise/language/llvm/trunk/lib/Transforms/Scalar/GVN.cpp:2154
  2154      bool removedBlock = MergeBlockIntoPredecessor(BB, this);
  event type: stop
  event type: break
  breakpoint number: 1
  break ...
  (gdb) p BB
  $1 = (llvm::BasicBlock *) 0x8a53bc0
(gdb) 
call void @ray_sphere_intersect(%struct._Isect* %occIsect, %struct._Ray* %ray, %struct._Sphere* getelementptr inbounds ([3 x %struct
call void @ray_sphere_intersect(%struct._Isect* %occIsect, %struct._Ray* %ray, %struct._Sphere* getelementptr inbounds ([3 x %struct
call void @ray_sphere_intersect(%struct._Isect* %occIsect, %struct._Ray* %ray, %struct._Sphere* getelementptr inbounds ([3 x %struct

使い方は

(1) breakpointを設定します。例では、GVN.cpp:2153です。
(2) tty t で、gdbでデバッグ実行するプログラムの出力先を"t"に変更しています
(3) pythonスクリプトの読み込みです。source pscript.py

上記の条件下でrunすると、BasicBlockのダンプの中にray_sphere_intersectがある場合に限りbreakできます。
※ちなみにサンプルのbitcodeはaobenchです。


python scriptingの説明を簡単に行うと、
(1) breakした際にcall backされるメソッドとして、breakpoint_stop_handlerを登録しています。
(2) breakpoint_stop_handlerの中では、BB->dump()の評価を行い、BBの中身を”t”に出力しています。
(3) ドロドロな方法を使っていますが、"t"を開いて"ray_sphere_intersect"が含まれているか検査しています。
(4) 目的の文字列がみつからなかった場合は、continueを実行して次のbreakpointへ飛びます。
(5) 目的の文字列が見つかった場合、普通にbreakして止まります。


上記のような方法がどういうときに役立つかというと、
(1) C++ STLに簡単に条件付きbreakをつけれそうな気がする。。自力で条件記述するのが面倒です。
(2) 処理系のプログラムでは、中間表現を部分的にダンプする処理を作り込むことが多いので、そのダンプ情報をパースしてbreakできる!!!

gdb python scriptingでしかできない操作というのはあまり思いつきませんが、
興味のある方は使ってみてはどうでしょう。
参考資料としては、gdbのソースコードのgdb/testsuite/gdb.pythonに、サンプルがたくさん入ってます。※

不満な点は、"t"を毎回openで開いているところです。
理想としては、fileに追加で書き込まれたところだけ検索したいのです。
誰か良い方法ご存知の方は教えてください。

----------------------------------------
Cのプログラムでは、readしたポインタを覚えておき、
そこから読み進めることが問題無くできるらしいのですが、
pythonでも同様に試してみましたがだめでした。
とりあえずは、検査済みの行数を覚えておき、読み飛ばそうかと思っています。


2011.12.23 *Fri

V8のJITコンパイラ、Crankshaftに関して

V8のJITコンパイラ、Crankshaftに関して、
概要を説明する資料を作成してみました。

JavaScript Advent Calendar 2011の22日目の記事でもあります。

全然javascriptじゃねーじゃん、、kernel VMいけよー後半コードはっつけただけじゃん、、
といわれたら、ごめんなさいするしかないのですが。。
途中経過をはっつけた状態なので、興味のあるところのコードを読んで、
もうちょっと綺麗にまとめる予定です。

V8Crankshaft

関連して、最近みて大変おもしろかったslideを紹介します。

virtual-machine-and-javascript-engine



vmやjavascript-engineの概要や歴史に関して説明しているスライドです。

その中で、old javascript engineはstack-baseを採用し、
modern javascript engineは、register-baseの中間表現を採用しているという下りがあります。

javascriptに限らないですが、確かにそういう感じはあります。
JVMやCILがstack-baseなのは歴史的な経緯なのだと思います。

stack-baseだと何が嬉しいのかというと、parseする際や、他にtranslateする際に、非常に楽です。
push-pop-push-popだけでオペランドや定義値の連鎖を表現できるので、
非常にシンプルなコードになります。

逆にregister-baseだと何がうれしいのかと言うと、たぶんSSA形式が関連しているように思います。
register-baseの場合、SSA形式の中間表現を採用していることが多いです。
SSA形式の場合、かならずregister-baseの中間表現になるはずなので、
結局SSA形式にするなら最初からregister-baseでいいじゃんっていうケースが多いのではないでしょうか。
register-baseの中間表現からSSA形式をボトムアップに解析して
SSA形式向けの情報を付加するケースもあるはずです。

この辺は歴史的な経緯があって、90年代はあまりSSA形式が主流じゃなかったので、
90年代以前のVMはstack-baseのものが多いのかなと勝手に思っています。



---追記
Crankshaftの紹介ページの誤字脱字や分かり難い表現など修正しました。
紹介ページ以外も今後追記していく予定です。

2011.12.03 *Sat

JITコンパイルに関して

※tracingに関しては、聞きかじったことを適当に書いてます。

JITコンパイルの一般的な説明ははぶきます。

JavaのJITコンパイラであるHotSpotが最も有名ですが、最近はたくさんあります。
最初にインタプリタ実行する実装を作ってから、JITを追加して高速化するケースや、
AOTが難しい動的言語の実装、および高速化によく使われています。

JITコンパイラの使用は、
AOTコンパイルとインタプリタ間のトレードオフを上手くコントロールすることができます。


良い 悪い
実行速度 AOT ≒ JIT >>> Interp
実行時のメモリ使用量 AOT > Interp >> JIT <-- Interpの位置は議論の余地あり
実行時のオーバーヘッド AOT > JIT >> Intep
実行前の準備 Interp > JIT >> AOT <-- 事前のコンパイルが必要
実行時の環境 Interp = JIT >> AOT <-- 環境に縛られない
作るコスト Interp >>> AOT > JIT <-- Interpは作るの簡単


基本的に、JITコンパイルは真ん中にいることが多いのですが、
JITコンパイルの中にも、様々な選択肢が存在します。


method compile
メソッド全体をJITコンパイルします。
traceと比較して適用範囲が広く、全体の性能が上がりやすいです。
traceと比較して、コンパイル時の、時間の増加、メモリ使用量の増加
ex) Java HotSpot

trace compile
狭い適用範囲に絞ってJITコンパイルします。
適用範囲を狭くすることにより、以下の利点を狙います。
コンパイル時の、時間の減少、メモリ使用量の減少
ex) Android
profilingやtracingのオーバーヘッドの減少
ex) なぞ
局所的にmethod compileより高速なコードを生成する
ex) TraceMonkey

trace compileの範囲
loop
よく実行されるループのみ
sequence block
シーケンシャルなブロックかつ、最適化の効果がありそうな場所


JITコンパイルは、profilingやtracingと組み合わせてよく使われます。
具体的には、インタプリタ実行時にプロファイル情報を取得し、
JITコンパイルする際に活用します。



profiling と tracing の違いははっきりいてよくわからないです。
tracingは、特定の箇所に絞って、大量の情報を取得するのかも。


各々の例を示すと、
profiling
メソッドの実行回数のカウント
--> JITコンパイルするかどうかの判定に利用
ループの実行回数のカウント
--> JITコンパイルするかどうかの判定に利用
--> ループ最適化のパラメータの最適化に利用
仮想関数呼び出しの、種類の統計
--> 脱仮想化に利用
calleeの引数の値の統計
--> 関数の特殊化に利用
分岐の統計
--> コード配置の最適化に利用 キャッシュや分岐予測を改善できる。
キャスト先の型の統計
--> checkcastを省略できるかも
DynamicCastの統計
--> instanceofを省略できるかも
Trapの統計
--> ヤバい関数は最適化しませんし、インタプリタに戻します。

tracing
上記に加えて、
変数の型の統計
--> TypeSpecializeにより、型が静的な区間を仮定。
変数の値の統計
--> 分岐潰し。
--> 内部クラスの仮定。


tracingの取得情報は多すぎて、オーバーヘッドが大きくなる可能性があります。
そのため、tracingで情報を取得する際の範囲を絞って、集中して情報を取得し、オーバーヘッドを軽減します。

よくJITコンパイラの利点として、実行時の情報を活用して最適化できることが挙げられます。
AOTコンパイルでも、実行時のカバレージやプロファイル情報を生成しておき、
別途コンパイラが食べれば、実行時の情報を活用できます。
一般的には、profile guided optimizationと呼ばれてます。

その時に問題になるのが、
プロファイル情報を取得する際の手間暇、
プロファイル情報取得時のオーバーヘッド、
プロファイル情報のサイズ、
だったりするように思います。


JITコンパイラの場合は、プロファイル情報の取得時のオーバーヘッドの軽減が考慮されている場合が多いですし、
最終コンパイルを終えた後は、プロファイル情報を捨てることが出来ます。

AOT向けのプロファイル情報の効率的な取得方法や、何を取得すべきかどうかなど、
あまり研究されていないのかも。大体がgcov連携だったりとか、、、
※ 私が知らないだけのような気もしますが。。


実行時の情報をprofilingしてAOTコンパイルやJITコンパイルに活用することは非常に面白いです。
将来的には、並列化に活用されたり、OSの独自命令を使用してprofilingしたりするのかもしれません。

2011.11.16 *Wed

Intel® Architecture Code Analyzerの使いどころ

Intel® Architecture Code Analyzer(以降IACAと略す)というのは、
sandy bridgeなどの最近のArchitectureのフロントエンドの動作をシミュレーションするツールです。

第2回 x86最適化勉強会で知りました。
IACAの紹介へのリンク

紹介されていた際にも、なんか使いどころがわからん???みたいな話だったのですが、
今回はIACAの用途を紹介したいと思います。

下記のリンクで紹介されているソースコードを例に、どんな出力になるのかを紹介したいと思います。

SSEを使って8flops/clockを実現する

試した環境は、
core i7 2600
gcc-4.6
です。

上記の紹介記事によると、2つの関数が出てきます。
sse_mulps_addps_no_dependency <-- nodepと略す
sse_mulps_addps_forwarding <-- fowardと略す

それぞれをO3でコンパイルし、どのような結果になるのかを紹介します。
※展開はとりあえずなしで

まずは、nodepのO3 展開なし版


---------------------------------------------------------------------------
Intel(R) Architecture Code Analyzer Version - 1.1.3
Analyzed File - nodep.gccO3.out
Binary Format - 32Bit
Architecture - Intel(R) AVX

Analysis Report
---------------
The analyzed block contains one or more instructions with issues.
The throughput and latency cycle counts do not account for those instructions.

Total Throughput: 5 Cycles; Throughput Bottleneck: FrontEnd, Port1
Total number of Uops bound to ports: 24
Data Dependency Latency: 10 Cycles; Performance Latency: 12 Cycles

Port Binding in cycles:
-------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 |
-------------------------------------------------------
| Cycles | 4 | 0 | 5 | 4 | 2 | 4 | 2 | 4 | 3 |
-------------------------------------------------------

N - port number, DV - Divider pipe (on port 0), D - Data fetch pipe (on ports
CP - on a critical Data Dependency Path
N - number of cycles port was bound
X - other ports that can be used by this instructions
F - Macro Fusion with the previous instruction occurred
^ - Micro Fusion happened
* - instruction micro-ops not bound to a port
@ - Intel(R) AVX to Intel(R) SSE code switch, dozens of cycles penalty is expe
! - instruction not supported, was not accounted in Analysis

| Num of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | |
------------------------------------------------------------
| ! | ! | ! | ! | ! | ! | ! | ! | ! | ! | | rdtsc
| 2^ | | | | 1 | | X | | 1 | | CP | mov dword ptr [esp
| 2^ | | | | X | | 1 | | 1 | | CP | mov dword ptr [esp
| 1 | | | | 1 | 1 | X | X | | | CP | mov ecx, dword ptr
| 1 | X | | X | | | | | | 1 | | mov eax, 0x200000
| 1 | | | | X | X | 1 | 1 | | | CP | mov ebx, dword ptr
| 1 | X | | X | | | | | | 1 | | nop
| 1 | | | 1 | | | | | | | | lea esi, ptr [esi]
| 1 | 1 | | | | | | | | | | mulps xmm1, xmm0
| 1 | | | 1 | | | | | | | | addps xmm3, xmm2
| 1 | 1 | | | | | | | | | | mulps xmm5, xmm4
| 1 | | | 1 | | | | | | | | addps xmm7, xmm6
| 1 | 1 | | | | | | | | | | mulps xmm1, xmm0
| 1 | | | 1 | | | | | | | | addps xmm3, xmm2
| 1 | 1 | | | | | | | | | | mulps xmm5, xmm4
| 1 | | | 1 | | | | | | | | addps xmm7, xmm6
| 1 | X | | X | | | | | | 1 | | sub eax, 0x1
| 0F | | | | | | | | | | | jnz 0xffffffe3
| ! | ! | ! | ! | ! | ! | ! | ! | ! | ! | | rdtsc
| 2^ | | | | 1 | | X | | 1 | | | mov dword ptr [esp
| 2^ | | | | X | | 1 | | 1 | | | mov dword ptr [esp
| 1 | | | | 1 | 1 | X | X | | | CP | mov esi, dword ptr
| 1 | | | | X | X | 1 | 1 | | | CP | mov edi, dword ptr
---------------------------------------------------------------------------


比較して、forwardのO3 展開なし版です。


---------------------------------------------------------------------------
Intel(R) Architecture Code Analyzer Version - 1.1.3
Analyzed File - foward.gccO3.out
Binary Format - 32Bit
Architecture - Intel(R) AVX

Analysis Report
---------------
The analyzed block contains one or more instructions with issues.
The throughput and latency cycle counts do not account for those instructions.

Total Throughput: 6 Cycles; Throughput Bottleneck: Port2_ALU, Port3_ALU
Total number of Uops bound to ports: 28
Data Dependency Latency: 13 Cycles; Performance Latency: 18 Cycles

Port Binding in cycles:
-------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 |
-------------------------------------------------------
| Cycles | 4 | 0 | 5 | 6 | 4 | 6 | 4 | 4 | 3 |
-------------------------------------------------------

N - port number, DV - Divider pipe (on port 0), D - Data fetch pipe (on ports
CP - on a critical Data Dependency Path
N - number of cycles port was bound
X - other ports that can be used by this instructions
F - Macro Fusion with the previous instruction occurred
^ - Micro Fusion happened
* - instruction micro-ops not bound to a port
@ - Intel(R) AVX to Intel(R) SSE code switch, dozens of cycles penalty is expe
! - instruction not supported, was not accounted in Analysis

| Num of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | |
------------------------------------------------------------
| ! | ! | ! | ! | ! | ! | ! | ! | ! | ! | | rdtsc
| 2^ | | | | 1 | | X | | 1 | | | mov dword ptr [esp
| 2^ | | | | X | | 1 | | 1 | | | mov dword ptr [esp
| 1 | | | | 1 | 1 | X | X | | | | mov ecx, dword ptr
| 1 | X | | X | | | | | | 1 | | mov eax, 0x200000
| 1 | | | | X | X | 1 | 1 | | | | mov ebx, dword ptr
| 1 | X | | X | | | | | | 1 | | nop
| 1 | | | 1 | | | | | | | | lea esi, ptr [esi]
| 2^ | 1 | | | 1 | 1 | X | X | | | CP | mulps xmm0, xmmwor
| 1 | | | 1 | | | | | | | CP | addps xmm4, xmm0
| 2^ | 1 | | | X | X | 1 | 1 | | | CP | mulps xmm1, xmmwor
| 1 | | | 1 | | | | | | | CP | addps xmm5, xmm1
| 2^ | 1 | | | 1 | 1 | X | X | | | CP | mulps xmm2, xmmwor
| 1 | | | 1 | | | | | | | CP | addps xmm6, xmm2
| 2^ | 1 | | | X | X | 1 | 1 | | | CP | mulps xmm3, xmmwor
| 1 | | | 1 | | | | | | | CP | addps xmm7, xmm3
| 1 | X | | X | | | | | | 1 | | sub eax, 0x1
| 0F | | | | | | | | | | | jnz 0xffffffdb
| ! | ! | ! | ! | ! | ! | ! | ! | ! | ! | | rdtsc
| 2^ | | | | 1 | | X | | 1 | | | mov dword ptr [esp
| 2^ | | | | X | | 1 | | 1 | | | mov dword ptr [esp
| 1 | | | | 1 | 1 | X | X | | | | mov esi, dword ptr
| 1 | | | | X | X | 1 | 1 | | | | mov edi, dword ptr
---------------------------------------------------------------------------


比較のポイントになるのは、
^ - Micro Fusion happened
のところ。

forwardのほうでは、mulpsとaddps間でmicro fusionしているらしく、スループットが2になってます。
この表示の見方が良く分かってないんですけど、
上下のどっちかのuopとくっついて、スループット2になってるんでしょうかね???

とりあえず、forwardのほうが速くなっている理由は、micro fusionが行われていることらしい。

直感的には、nodepのほうでport0とport1を順に使ってるんだから、
同時に実行してくれてもいいんじゃないか、いいんじゃないか?という気がします。
私はプロセッサ屋じゃないので、よくわからんです。

何となく上記の結果を見て、
内部でmicro fusionした結果、積和演算命令と同じuopに変換されたってことなのかな?
と思い、x86の積和演算を探して実行して比較してみることにしました。
積和演算はAVXと一緒にFMAがあったよなと思い、
vfmadd132ps %ymm1, %ymm3, %ymm5
とか書いてみたけど、illegal instructionでこけて測定できない。
後で知ったのですが、sandy bridgeではFMA未実装らしい。。。

お次に試したのは、AVXのvfmulpsやvfaddpsではどうなるのかなということ。
そしたら驚きの結果が、、
こちらの結果は、fmul/faddと同じだったのですが、

---------------------------------------------------------------------------
asm volatile
(
"vmulps %0, %%xmm0, %%xmm0\n\t"
"vaddps %%xmm0, %%xmm1, %%xmm1\n\t"
"vmulps %0, %%xmm2, %%xmm2\n\t"
"vaddps %%xmm2, %%xmm3, %%xmm3\n\t"
:
:"m"(a[0])
);
---------------------------------------------------------------------------

3opであることを活かし、destとの依存を試しに切ってみたら、、

---------------------------------------------------------------------------
asm volatile
(
"vmulps %0, %%xmm0, %%xmm4\n\t"
"vaddps %%xmm4, %%xmm1, %%xmm1\n\t"
"vmulps %0, %%xmm2, %%xmm5\n\t"
"vaddps %%xmm5, %%xmm3, %%xmm3\n\t"
:
:"m"(a[0])
);
---------------------------------------------------------------------------



-- sse_mulps_addps_no_dependency --
GFLOPS @ 3.40GHz:
  3.009 [flops/clock] = 10.232 [GFLOPS]  (67108864 flops in 22300090 clock = 0.006559 sec)
Throughput:
  0.752 [instructions/clock]   (16777216 instrucions in 22300090 clock)
-- sse_mulps_addps_forwarding --
GFLOPS @ 3.40GHz:
  5.611 [flops/clock] = 19.077 [GFLOPS]  (67108864 flops in 11960588 clock = 0.003518 sec)
Throughput:
  1.403 [instructions/clock]   (16777216 instrucions in 11960588 clock)
-- avx_vmulps_vaddps_forwarding --
GFLOPS @ 3.40GHz:
  11.408 [flops/clock] = 38.788 [GFLOPS]  (67108864 flops in 5882441 clock = 0.001730 sec)
Throughput:
  2.852 [instructions/clock]   (16777216 instrucions in 5882441 clock)


なんと従来秘の3倍、スループット3倍デスヨ


IACAの解析結果

---------------------------------------------------------------------------
Intel(R) Architecture Code Analyzer Version - 1.1.3
Analyzed File - test003.O3.out
Binary Format - 32Bit
Architecture - Intel(R) AVX

Analysis Report
---------------
The analyzed block contains one or more instructions with issues.
The throughput and latency cycle counts do not account for those instructions.

Total Throughput: 5 Cycles; Throughput Bottleneck: Port2_ALU, Port3_ALU
Total number of Uops bound to ports: 21
Data Dependency Latency: 13 Cycles; Performance Latency: 16 Cycles

Port Binding in cycles:
-------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 |
-------------------------------------------------------
| Cycles | 3 | 0 | 2 | 5 | 3 | 5 | 3 | 4 | 2 |
-------------------------------------------------------

N - port number, DV - Divider pipe (on port 0), D - Data fetch pipe (on ports
CP - on a critical Data Dependency Path
N - number of cycles port was bound
X - other ports that can be used by this instructions
F - Macro Fusion with the previous instruction occurred
^ - Micro Fusion happened
* - instruction micro-ops not bound to a port
@ - Intel(R) AVX to Intel(R) SSE code switch, dozens of cycles penalty is expe
! - instruction not supported, was not accounted in Analysis

| Num of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | |
------------------------------------------------------------
| ! | ! | ! | ! | ! | ! | ! | ! | ! | ! | | rdtsc
| 2^ | | | | 1 | | X | | 1 | | | mov dword ptr [ebp
| 2^ | | | | X | | 1 | | 1 | | | mov dword ptr [ebp
| 1 | | | | 1 | 1 | X | X | | | | mov ecx, dword ptr
| 0* | | | | | | | | | | | xor eax, eax
| 1 | | | | X | X | 1 | 1 | | | | mov ebx, dword ptr
| 1 | 1 | | X | | | | | | X | | data16 nop
| 2^ | 1 | | | 1 | 1 | X | X | | | CP | vmulps xmm4, xmm0,
| 1 | | | 1 | | | | | | | CP | vaddps xmm1, xmm1,
| 2^ | 1 | | | X | X | 1 | 1 | | | CP | vmulps xmm5, xmm2,
| 1 | | | 1 | | | | | | | CP | vaddps xmm3, xmm3,
| 1 | X | | X | | | | | | 1 | | add eax, 0x1
| 1 | X | | X | | | | | | 1 | | cmp eax, 0x200000
| 0F | | | | | | | | | | | jnz 0xffffffe4
| ! | ! | ! | ! | ! | ! | ! | ! | ! | ! | | rdtsc
| 2^ | | | | 1 | | X | | 1 | | | mov dword ptr [ebp
| 2^ | | | | X | | 1 | | 1 | | | mov dword ptr [ebp
| 1 | | | | 1 | 1 | X | X | | | | mov esi, dword ptr
| 1 | | | | X | X | 1 | 1 | | | | mov edi, dword ptr
---------------------------------------------------------------------------


うーん、forwardとほぼ同じです。
OoOのほうで、何かいいことあるのでしょうかね。

とりあえずperfで調べてみる。
perfの使いかた

最初のmulpsとaddpsで版

---------------------------------------------------------------------------
elise@elise-desktop:~/language/icc/work/sse$ perf stat forward.out
-- sse_mulps_addps_forwarding --
GFLOPS @ 3.40GHz:
6.686 [flops/clock] = 22.733 [GFLOPS] (67108864 flops in 10036803 clock = 0.002952 sec)
Throughput:
1.672 [instructions/clock] (16777216 instrucions in 10036803 clock)

Performance counter stats for 'forward.out':

3.506812 task-clock-msecs # 0.951 CPUs
1 context-switches # 0.000 M/sec
0 CPU-migrations # 0.000 M/sec
115 page-faults # 0.033 M/sec
12606988 cycles # 3595.000 M/sec
10922084 instructions # 0.866 IPC
9044 cache-references # 2.579 M/sec
2792 cache-misses # 0.796 M/sec

0.003687630 seconds time elapsed
---------------------------------------------------------------------------



AVXのvmulpsとvaddps(レジスタはsrcを破壊的に使用)

---------------------------------------------------------------------------
elisese@elise-desktop:~/language/icc/work/sse$ perf stat avxforward2.out
-- avx_vmulps_vaddps_forwarding2 --
GFLOPS @ 3.40GHz:
6.721 [flops/clock] = 22.853 [GFLOPS] (67108864 flops in 9984227 clock = 0.002937 sec)
Throughput:
1.680 [instructions/clock] (16777216 instrucions in 9984227 clock)

Performance counter stats for 'avxforward2.out':

3.477048 task-clock-msecs # 0.945 CPUs
1 context-switches # 0.000 M/sec
0 CPU-migrations # 0.000 M/sec
114 page-faults # 0.033 M/sec
12495889 cycles # 3593.821 M/sec
10922680 instructions # 0.874 IPC
8583 cache-references # 2.468 M/sec
2166 cache-misses # 0.623 M/sec

0.003680900 seconds time elapsed
---------------------------------------------------------------------------


AVXのvmulpsとvaddps(srcとdestは別レジスタの版)

---------------------------------------------------------------------------
elise@elise-desktop:~/language/icc/work/sse$ perf stat avxforward.out
-- avx_vmulps_vaddps_forwarding --
GFLOPS @ 3.40GHz:
11.272 [flops/clock] = 38.325 [GFLOPS] (67108864 flops in 5953505 clock = 0.001751 sec)
Throughput:
2.818 [instructions/clock] (16777216 instrucions in 5953505 clock)

Performance counter stats for 'avxforward.out':

2.308656 task-clock-msecs # 0.932 CPUs
0 context-switches # 0.000 M/sec
0 CPU-migrations # 0.000 M/sec
115 page-faults # 0.050 M/sec
8304734 cycles # 3597.216 M/sec
10915629 instructions # 1.314 IPC
8383 cache-references # 3.631 M/sec
2481 cache-misses # 1.075 M/sec

0.002476608 seconds time elapsed
---------------------------------------------------------------------------


IPCの値が高いのは分かるんだけど、IACAの分析ではフロントまで同じなんだよね。
謎です。。
avx_sample
CATEGORY : 日記 | THEME : ソフトウェア開発 | GENRE : コンピュータ

検索フォーム



プロフィール

nothingcosmos

Author:nothingcosmos
コンパイラと投資に関して
自分の考えや、周辺技術の紹介等をしています。
コメント・質問はお気軽にどうぞ。

LLVM wiki
twitter:nothingcosmos



リンク

このブログをリンクに追加する



最新記事



カテゴリ



最新トラックバック



月別アーカイブ



05
--
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
--
Copyright © LLVMとコンパイラと投資 All Rights Reserved.
テンプレート配布者: サリイ  ・・・  素材: bee  ・・・  FC2ブログ