Swift解体新書 その1 ニーモニック確認環境

通常ならswift de classDrivenの続きのはずだが、一旦中断してクロスコンパイル環境から解体することにした。

 

なぜなら、WWDC2015 - 408にむっちゃ恐ろしいことが発表されていたから...

忙しさに負けて、チェック抜け...間抜け

 

詳しい解説はネット上和訳が出回っているのでここでは割愛するが、これによるとAppleは(少なくともswiftのPLであろうabraham氏によれば)、

継承構造は継承と言いつつ資源配分問題になると、

途端に厄介な問題を抱え込むので、そこは正式にはサポらんよ。

Protocol-Orientedで書いてね、てへぺろ

 と、いろいろ誤解を招く訳しかたをしたが、言いたいことはclass/struct/enumで継承使って親クラスの資源を無考で使えると思うなよ、そもそもabstractみたいな概念自体わしゃ好かん。

そんな細かいこというやつは、protocol+extension(これは明言してないけどObjCならprotocol+Category)で記述しろと。。。

 

これつまり、Appleはダブルチェックロッキングとかめんどくさいこと考えたくないから、独自路線を行きます。JavaやらC++と同じ概念で継承を考えると細かい所で違うものになるから痛い目見るよと(独自解釈)。

Alan Kay先輩とBjarne Stroustrup先輩の視点差に関して、ObjCでは業界トレンドに任せてごまかしていたことをついに認めたコメントなんだと思う(独自解釈)。

 

 

ということで、基本に立ち返る。

言われてみれば、swiftcも1.1時代にobjCとたいして変わらんことしか確認せず、まだ具体的には触ったことがなかったのでちょっと触ってみた。

最終Goalはニーモニックでクラス階層構造を確認すること。

この記事のGoalはとりあえず.o, .sファイル相当の内容確認ができるところまで。

 

---

閑話休題。では、以下本日のゴールに向かって順番に。

まずはswiftc  これがないとはじまらない。

これはxcodeなり、swift.orgからなりtoolchain取ってくれば入っているはず。

$ swiftc -v

Apple Swift version 2.2 (swiftlang-703.0.18.1 clang-703.0.29)

Target: x86_64-apple-macosx10.9

 

 素知らぬ顔して、clangがLLVMのフロントにあることを告白している。

前に見たときこんなの出力してたっけ? そんななわかりきってるので、今日は触れない。

 

次、binutils これは、swiftc -SなりxcodeのDebug Workflow > Dissassembly でも代用はできるけど、サマるんだったら入れておいて損はない。

macならbrewで入れればまず問題は無いだろう。EL-CapitanのSIP問題に引っかかったら、ググって自己解決してください。

Linuxならapt-get

ここではgobjdumpが使えれば必要十分。

 

解析ツールはそろったので、次それぞれの使い方に進みます。

swiftc

これは、コンパイルするのに使う。

とりあえずの試料としてざざっとLoopテストを書いてみた。

asの確認をするのであれば平でfuncを書いた方が分かりやすいが、とりあえず。。。あとprobe効果なんかもほぼ考慮していないので、試料内容に関する突っ込みはご容赦いただきたい。

もちろん配列がL2に入るかなんて、とりあえず数回して誤差詰めとけ位にしか考えていない。

 

 

 Playgroundで書き出したが、XcodeのREPLの起こし方がアホで、編集の度にインタプリタ実行をかけ直そうとする。Loop試験のような何百万もループ回すコードだとさすがに固まるため、途中でコピペしてAtomでやった。

たぶんなんらかXcode設定はあるかと思うが、どちらにせよREPLだとs/n比低すぎてLoopの試験にはならんので、結局コンパイルする予定だからあまりこの場では深く考えないものとする。

あくまでGoalはニーモニック確認

 

 で、この試料をswiftcで実行ファイルにビルドする。

$ swiftc -O -sdk $(xcrun --show-sdk-path --sdk macosx) -o aStruct.out for-inExp-struct.swift

どうやらエントリポイント(main)はfoundationにリンクすることで、バイパスしているらしい。普通はこれで走るわけはないので注意。

 

今どきはアルゴリズムが成熟しているため、-Oを付けた方がより実測値に近づくがこれはお好み。 

--sdkはiphoneos(全小文字)にすればiOS向けにクロスできるけど、objdumpが欲しいだけなので、x86でいいだろう。

試料でUIKitではなくFoundationをimportしているのはそういう理由。

 

$ aStruct.out

でターミナルなんかから直接実行されるファイルができあがる。

もろもろ面倒なので、script書いた

 

OUTPUTはこんな感じ

StructTrials

opened for : av = 59.826675, total = 5982.667542

closed for : av = 26.785226, total = 2678.522583

for-in : av = 27.640303, total = 2764.030273

zipIndexing : av = 61.732862, total = 6173.286194

while : av = 59.986423, total = 5998.642334

zip、whileはなんとなく想像ついたけど、開区間(...) 閉区間(..<)でこんな違うんだ。

zip同様Loopごとに余計な命令群でも挟まってるんだろう。この辺は、dumpoファイルを元に解析予定なので後述(たぶん)

Pythonがrange使ってる理由がなんか解る気がする。

 

AppleDevにはfor-inで書けとしか書いてなかったんで、swiftでは実質高速列挙なんだろうな。ObjCではenumerate...blocksと挙動が違ったような覚えあるけど。

ほんとはfor(;;)も比較対象に入れたかったけど、syntaxに無かったようなので断念。

この辺の考察も想像の羽を広げてる場合ではないので、後述。

 

gobjdump

gnuなのでgas, gcc同様、頭にgが付く。

gobjdump -S -d aStruct.out > objStruct.txt

こんな短いコードだけど、FPテクを使ってるからか全貌はかなりの長さ。

以下、callqで飛んでいる興味のあるところだけだらだらコピペする

0000000100002090 <__TFV7aStruct7runTest11letUsLoopinfT_T_>:
100002090: push %rbp
100002091: mov %rsp,%rbp
100002094: push %r15
100002096: push %r14
100002098: push %rbx
100002099: push %rax
10000209a: mov %rdi,%rbx

// reset()
10000209d: movq $0x0,0x4b10(%rip) # 100006bb8 <__Tv7aStruct5totalSi>
1000020a4: 00 00 00 00

// self.myFunc() のはず
1000020a8: callq 100005314 <__ZL20fixStringForCoreDataP11objc_object+0xc9>
....

__ZL20fixStringForCoreDataP11objc_objectは0x10000524bとあるので、offset0x9cで

0x100005314へ。

100005314: jmpq   *0xdee(%rip)        # 100006108 <__ZL17_load_method_type+0x267> 

 

0000000100006108 <__DATA.__la_symbol_ptr>:
100006108: in (%dx),%al 

...

うーーん、anonymous関数ポインタのハンドリング...で、これ以上読み進める自信が無いので、すこしFP の手を緩める。

FPでは値も処理も区別しないため、何段階かアプローチはあるのだけれど、MAXひよってこうした。

 

もはや、関数ポインタを即値で代入しているだけで、何がしたいのかよくわからないが、おかげでdumpファイルが100K近く小さくなってくれた。

つまり固定値ラベルを追えばいいだけに。

 

いや、目的を考えたら、かっこつけずに初手からこうすべき話なのは解っていたのだが。。。FP+OOP脳に邪魔された。

 

stack操作はノイズだとして、各こういった対応になる

closed for -> av = 26.785226

for i in 0 ..< testValues.count {
        total += testValues[i]

 

0000000100002200 <__TF7aStruct6test01FT_T_>:

....

....

10000220b: mov 0x10(%rax),%rcx

10000220f: test %rcx,%rcx

100002212: je 100002235 <__TF7aStruct6test01FT_T_+0x35>

100002214: mov 0x499d(%rip),%rdx # 100006bb8 <__Tv7aStruct5totalSi>

10000221b: add $0x20,%rax

10000221f: nop

100002220: add (%rax),%rdx

100002223: mov %rdx,0x498e(%rip) # 100006bb8 <__Tv7aStruct5totalSi>

10000222a: jo 100002237 <__TF7aStruct6test01FT_T_+0x37>

10000222c: add $0x8,%rax

100002230: dec %rcx

100002233: jne 100002220 <__TF7aStruct6test01FT_T_+0x20>

100002235: pop %rbp

100002236: retq 

100002237: ud2 

100002239: nopl 0x0(%rax)

 オーバーフローでオペコード例外とか。

インデクスはrcxに最大値取ってからdecしてるけど、一般的な条件分岐jmpらしい。全てがレジスタで完結しているし、妥当な最適化がされているから速い。

 

open for ->  av = 59.826675

let max = testValues.count - 1
for i in 0...max {
        total += testValues[i]
}

0000000100002240 <__TF7aStruct6test02FT_T_>:
....
100002244: mov 0x4965(%rip),%rax # 100006bb0 <__Tv7aStruct10testValuesGSaSi_>
10000224b: mov 0x10(%rax),%rcx
10000224f: test %rcx,%rcx
100002252: je 100002287 <__TF7aStruct6test02FT_T_+0x47>
100002254: dec %rcx
100002257: xor %edx,%edx
100002259: nopl 0x0(%rax)
100002260: mov 0x4951(%rip),%rsi # 100006bb8 <__Tv7aStruct5totalSi>
100002267: add 0x20(%rax,%rdx,8),%rsi
10000226c: mov %rsi,0x4945(%rip) # 100006bb8 <__Tv7aStruct5totalSi>
100002273: jo 100002283 <__TF7aStruct6test02FT_T_+0x43>
100002275: cmp %rdx,%rcx
100002278: je 100002285 <__TF7aStruct6test02FT_T_+0x45>
10000227a: inc %rdx
10000227d: cmp 0x10(%rax),%rdx
100002281: jb 100002260 <__TF7aStruct6test02FT_T_+0x20>
100002283: ud2
100002285: pop %rbp
100002286: retq
100002287: ud2
100002289: nopl 0x0(%rax)

このレベルだと、letによる参照透過性の違いは見られない。 

そして、cmp %rdx, rcxあたり怪しすぎる。rcxとrdx二本ふりまわしてdec,inc繰り返してるのか。

retqの前にud2が来てるのもなんかおかしい気もするが、こういうものだっけ?

-Oつけてもこれだと言うことは、..<はなんらか展開しくってるように思える。

実行速度も出てないし、開区間指定はまだこなれていないのかもしれない。

そもそも、for文は昔から0からはじまる閉区間なので、需要があるまではあまり力入れてないのかも

 

for-in  -> av = 27.640303

for v in testValues {
        total += v
}

0000000100002290 <__TF7aStruct6test03FT_T_>:
....
100002294: mov 0x4915(%rip),%rax # 100006bb0 <__Tv7aStruct10testValuesGSaSi_>
10000229b: mov 0x10(%rax),%rcx
10000229f: test %rcx,%rcx
1000022a2: je 1000022c5 <__TF7aStruct6test03FT_T_+0x35>
1000022a4: mov 0x490d(%rip),%rdx # 100006bb8 <__Tv7aStruct5totalSi>
1000022ab: add $0x20,%rax
1000022af: nop
1000022b0: add (%rax),%rdx
1000022b3: mov %rdx,0x48fe(%rip) # 100006bb8 <__Tv7aStruct5totalSi>
1000022ba: jo 1000022c7 <__TF7aStruct6test03FT_T_+0x37>
1000022bc: add $0x8,%rax
1000022c0: dec %rcx
1000022c3: jne 1000022b0 <__TF7aStruct6test03FT_T_+0x20>
1000022c5: pop %rbp
1000022c6: retq
1000022c7: ud2
1000022c9: nopl 0x0(%rax)

あーなるほど。testValuesの次アイテムを取り出すのに、raxをアドレスぶんオフセットかけてるのか。 closed forと極めて似ている。処理時間も近似している。

 

while -> av = 59.986423
var test = testValues.count - 1
while test > 0 {
        total += testValues[test]
        test -= 1

<__TF7aStruct6test04FT_T_>:
1000022d0: push %rbp
1000022d1: mov %rsp,%rbp
1000022d4: mov 0x48d5(%rip),%rax # 100006bb0 <__Tv7aStruct10testValuesGSaSi_>
1000022db: mov 0x10(%rax),%rcx
1000022df: cmp $0x2,%rcx
1000022e3: jb 100002317 <__TF7aStruct6test04FT_T_+0x47>
1000022e5: test %rcx,%rcx
1000022e8: je 100002315 <__TF7aStruct6test04FT_T_+0x45>
1000022ea: dec %rcx
1000022ed: nopl (%rax)
1000022f0: mov 0x48c1(%rip),%rdx # 100006bb8 <__Tv7aStruct5totalSi>
1000022f7: add 0x20(%rax,%rcx,8),%rdx
1000022fc: mov %rdx,0x48b5(%rip) # 100006bb8 <__Tv7aStruct5totalSi>
100002303: jo 100002315 <__TF7aStruct6test04FT_T_+0x45>
100002305: dec %rcx
100002308: jo 100002315 <__TF7aStruct6test04FT_T_+0x45>
10000230a: test %rcx,%rcx
10000230d: jle 100002317 <__TF7aStruct6test04FT_T_+0x47>
10000230f: cmp 0x10(%rax),%rcx
100002313: jb 1000022f0 <__TF7aStruct6test04FT_T_+0x20>
100002315: ud2
100002317: pop %rbp
100002318: retq
100002319: nopl 0x0(%rax)
100002320: push %rbp
100002321: mov %rsp,%rbp
100002324: push %r14
100002326: push %rbx
100002327: mov 0x45d2(%rip),%rbx # 100006900 <__ZL12demangleLock+0x40>
10000232e: test %rbx,%rbx
100002331: jne 1000023b9 <__TF7aStruct6test04FT_T_+0xe9>
100002337: mov $0x20,%edi
10000233c: mov $0x7,%esi
100002341: callq 1000054b8 <__ZL20fixStringForCoreDataP11objc_object+0x26d>
100002346: mov %rax,%rbx
100002349: mov 0x3cd8(%rip),%rax # 100006028 <__ZL17_load_method_type+0x187>
100002350: mov %rax,(%rbx)
100002353: mov 0x4496(%rip),%rax # 1000067f0 <__TMLFT_T_>
10000235a: test %rax,%rax
10000235d: jne 10000237e <__TF7aStruct6test04FT_T_+0xae>
10000235f: mov 0x3cda(%rip),%rsi # 100006040 <__ZL17_load_method_type+0x19f>
100002366: add $0x8,%rsi
10000236a: mov $0x1,%edi
10000236f: mov %rsi,%rdx
100002372: callq 100005488 <__ZL20fixStringForCoreDataP11objc_object+0x23d>
100002377: mov %rax,0x4472(%rip) # 1000067f0 <__TMLFT_T_>
10000237e: mov %rax,0x8(%rbx)
100002382: movq 0x3ca6(%rip),%xmm0 # 100006030 <__ZL17_load_method_type+0x18f>
100002389: pshufd $0x44,%xmm0,%xmm0
10000238f: movdqu %xmm0,0x10(%rbx)
100002394: xor %eax,%eax
100002396: lock cmpxchg %rbx,0x4561(%rip) # 100006900 <__ZL12demangleLock+0x40>
10000239d: mov %rax,%r14
1000023a2: je 1000023b9 <__TF7aStruct6test04FT_T_+0xe9>
1000023a4: mov $0x20,%esi
1000023a9: mov $0x7,%edx
1000023ae: mov %rbx,%rdi
1000023b1: callq 1000054be <__ZL20fixStringForCoreDataP11objc_object+0x273>
1000023b6: mov %r14,%rbx
1000023b9: mov %rbx,%rax
1000023bc: pop %rbx
1000023bd: pop %r14
1000023bf: pop %rbp
1000023c0: retq

やめてくれ!もはや見る気がしない。zipはfuncに立てるのを忘れたが、なんか皆察しの通りかと。

 

objdumpからニーモニック人力評価のための環境構築というGoalは満たせたし、

長くなりすぎて何がしたいのかが不明瞭になってきたので、今日はこの辺にしておこう。

 

 気が向いたら次は今回大幅に日和ったラムダ式の追い方か、いきなり核心を突いて継承とprotocol+extesionの違いを見てみよう。