投機実行とOut of Orderはつよいというはなし
fluentdな人達と話していると定期的にRubyのオブジェクト生成が遅いとdisられます。
本質的にしょうがない部分もあるんですが、それにしても遅い部分も結構あるので、おもむろにperf statとperf recordを取ってみましょう。
% sudo perf stat -d ./miniruby -e'GC.disable;i=1000000;while (i-=1)>0;Hash.new;end'
Performance counter stats for './miniruby -eGC.disable;i=1000000;while (i-=1)>0;Hash.new;end':
467.629812 task-clock (msec) # 0.993 CPUs utilized
19 context-switches # 0.041 K/sec
2 cpu-migrations # 0.004 K/sec
65547 page-faults # 0.140 M/sec
1356052041 cycles # 2.900 GHz [49.17%]
683406025 stalled-cycles-frontend # 50.40% frontend cycles idle [49.73%]
<not supported> stalled-cycles-backend
1656503729 instructions # 1.22 insns per cycle
# 0.41 stalled cycles per insn [63.34%]
330462548 branches # 706.676 M/sec [63.60%]
447683 branch-misses # 0.14% of all branches [63.93%]
<not supported> L1-dcache-loads:HG
9560448 L1-dcache-load-misses:HG # 0.00% of all L1-dcache hits [63.35%]
0 LLC-loads:HG # 0.000 K/sec [62.52%]
0 LLC-load-misses:HG # 0.00% of all LL-cache hits [49.22%]
0.470869900 seconds time elapsed
% sudo perf record ./miniruby -e'GC.disable;i=1000000;while (i-=1)>0;Hash.new;end'
Lowering default frequency rate to 1750.
Please consider tweaking /proc/sys/kernel/perf_event_max_sample_rate.
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.036 MB perf.data (~1585 samples) ]
% sudo perf report --stdio
# ========
# captured on: Wed Apr 20 15:09:24 2016
# hostname : tk2-243-31075
# os release : 3.13.0-77-generic
# perf version : 3.13.11-ckt32
# arch : x86_64
# nrcpus online : 3
# nrcpus avail : 3
# cpudesc : Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz
# cpuid : GenuineIntel,6,62,4
# total memory : 2049828 kB
# cmdline : /usr/lib/linux-tools-3.13.0-77/perf record ./miniruby -eGC.disable;i=1000000;while (i-=1
)>0;Hash.new;end
# event : name = cycles, type = 0, config = 0x0, config1 = 0x0, config2 = 0x0, excl_usr = 0, excl_ke
rn = 0, excl_host = 0, excl_guest = 1, precise_ip = 0, attr_mmap2 = 0, attr_mmap = 1, attr_mmap_dat
a = 0
# HEADER_CPU_TOPOLOGY info available, use -I to display
# HEADER_NUMA_TOPOLOGY info available, use -I to display
# pmu mappings: cpu = 4, software = 1, tracepoint = 2, breakpoint = 5
# ========
#
# Samples: 1K of event 'cycles'
# Event count (approx.): 1582794433
#
# Overhead Command Shared Object Symbol
# ........ ........ .................. ..........................................
#
13.72% miniruby [kernel.kallsyms] [k] clear_page_c_e
9.49% miniruby miniruby [.] vm_exec_core
7.36% miniruby libc-2.19.so [.] _int_malloc
6.15% miniruby miniruby [.] objspace_malloc_increase.isra.67
5.06% miniruby libc-2.19.so [.] malloc
4.10% miniruby libpthread-2.19.so [.] pthread_getspecific
3.97% miniruby libc-2.19.so [.] __libc_calloc
3.78% miniruby [kernel.kallsyms] [k] get_page_from_freelist
3.21% miniruby miniruby [.] rb_wb_protected_newobj_of
3.12% miniruby miniruby [.] rb_obj_call_init
2.98% miniruby miniruby [.] ruby_xcalloc
2.74% miniruby [kernel.kallsyms] [k] async_page_fault
2.28% miniruby miniruby [.] vm_call0_body.constprop.120
1.75% miniruby miniruby [.] vm_call_cfunc
1.44% miniruby miniruby [.] st_init_table_with_size
1.41% miniruby miniruby [.] rb_callable_method_entry
1.10% miniruby miniruby [.] rb_call0
1.08% miniruby [kernel.kallsyms] [k] handle_mm_fault
1.02% miniruby miniruby [.] vm_search_method.isra.77
0.95% miniruby [kernel.kallsyms] [k] __alloc_pages_nodemask
0.94% miniruby [kernel.kallsyms] [k] __mem_cgroup_commit_charge
0.86% miniruby [kernel.kallsyms] [k] __rmqueue
[…]
上位にカーネルやlibc、RubyVM実行エンジンの中心部であるvm_exec_coreが並んでいて、「あ、これは戦う敵を間違えたかな」って雰囲気が若干漂っていますが、強引に目に入ったruby_xcallocに食らいついていきます。いや、こいつただのcallocのラッパのくせに3%近く食ってて生意気じゃないですか。
とりあえずobjdumpで中身を眺めてみましょう。
% ruby -e'/\A0*(\h+)[ .a-z]*\t0*(\h+)/i=~`objdump -t miniruby|grep #{ARGV[0]||abort}`and exec"objdump -d --start-address=0x#{$1} --stop-address=#{"%#x"%[$1.hex+$2.hex]} miniruby|less"' ruby_xcalloc
miniruby: file format elf64-x86-64
Disassembly of section .text:
0000000000496280 <ruby_xcalloc>:
496280: 48 8d 05 51 7a 44 00 lea 0x447a51(%rip),%rax # 8ddcd8 <ruby_current_vm>
496287: 41 54 push %r12
496289: 31 d2 xor %edx,%edx
49628b: 55 push %rbp
49628c: 53 push %rbx
49628d: 48 8b 00 mov (%rax),%rax
496290: 4c 8b a0 30 06 00 00 mov 0x630(%rax),%r12
496297: 48 c7 c0 ff ff ff ff mov $0xffffffffffffffff,%rax
49629e: 48 f7 f6 div %rsi
4962a1: 48 39 c7 cmp %rax,%rdi
4962a4: 0f 87 43 01 00 00 ja 4963ed <ruby_xcalloc+0x16d>
4962aa: 48 89 fb mov %rdi,%rbx
4962ad: 48 0f af de imul %rsi,%rbx
4962b1: 48 85 db test %rbx,%rbx
4962b4: 0f 88 96 00 00 00 js 496350 <ruby_xcalloc+0xd0>
4962ba: b8 01 00 00 00 mov $0x1,%eax
4962bf: 48 0f 44 d8 cmove %rax,%rbx
4962c3: 41 f6 44 24 10 40 testb $0x40,0x10(%r12)
4962c9: 75 45 jne 496310 <ruby_xcalloc+0x90>
4962cb: 48 89 de mov %rbx,%rsi
4962ce: bf 01 00 00 00 mov $0x1,%edi
4962d3: e8 68 11 f8 ff callq 417440 <calloc@plt>
4962d8: 48 89 c5 mov %rax,%rbp
4962db: 48 85 c0 test %rax,%rax
4962de: 0f 84 a4 00 00 00 je 496388 <ruby_xcalloc+0x108>
4962e4: 48 89 ef mov %rbp,%rdi
4962e7: e8 84 10 f8 ff callq 417370 <malloc_usable_size@plt>
4962ec: 4c 89 e7 mov %r12,%rdi
4962ef: b9 01 00 00 00 mov $0x1,%ecx
4962f4: 31 d2 xor %edx,%edx
4962f6: 48 89 c6 mov %rax,%rsi
4962f9: e8 e2 f7 ff ff callq 495ae0 <objspace_malloc_increase.isra.67>
4962fe: 48 89 e8 mov %rbp,%rax
496301: 5b pop %rbx
496302: 5d pop %rbp
496303: 41 5c pop %r12
496305: c3 retq
496306: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
49630d: 00 00 00
496310: e8 fb 2f 16 00 callq 5f9310 <ruby_native_thread_p>
496315: 85 c0 test %eax,%eax
496317: 74 b2 je 4962cb <ruby_xcalloc+0x4b>
496319: 49 8b 84 24 b0 01 00 mov 0x1b0(%r12),%rax
496320: 00
496321: 89 c6 mov %eax,%esi
496323: 83 e6 01 and $0x1,%esi
496326: 74 09 je 496331 <ruby_xcalloc+0xb1>
496328: 48 c1 e8 03 shr $0x3,%rax
49632c: 89 c6 mov %eax,%esi
49632e: 83 e6 01 and $0x1,%esi
496331: 41 b8 00 12 00 00 mov $0x1200,%r8d
496337: b9 01 00 00 00 mov $0x1,%ecx
49633c: ba 01 00 00 00 mov $0x1,%edx
496341: 4c 89 e7 mov %r12,%rdi
496344: e8 d7 f6 ff ff callq 495a20 <garbage_collect_with_gvl>
496349: eb 80 jmp 4962cb <ruby_xcalloc+0x4b>
49634b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
496350: e8 9b a2 15 00 callq 5f05f0 <ruby_thread_has_gvl_p>
496355: 85 c0 test %eax,%eax
496357: 0f 85 a1 00 00 00 jne 4963fe <ruby_xcalloc+0x17e>
49635d: e8 ae 2f 16 00 callq 5f9310 <ruby_native_thread_p>
496362: 85 c0 test %eax,%eax
496364: 0f 84 88 00 00 00 je 4963f2 <ruby_xcalloc+0x172>
49636a: 48 8d 35 af 1a 17 00 lea 0x171aaf(%rip),%rsi # 607e20 <null_device+0x1378>
496371: 48 8d 3d 28 3f ff ff lea -0xc0d8(%rip),%rdi # 48a2a0 <negative_size_allocation_error_with_gvl>
496378: e8 33 e5 15 00 callq 5f48b0 <rb_thread_call_with_gvl>
49637d: e9 41 ff ff ff jmpq 4962c3 <ruby_xcalloc+0x43>
496382: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
496388: 41 b8 00 02 00 00 mov $0x200,%r8d
49638e: b9 01 00 00 00 mov $0x1,%ecx
496393: ba 01 00 00 00 mov $0x1,%edx
496398: be 01 00 00 00 mov $0x1,%esi
49639d: 4c 89 e7 mov %r12,%rdi
4963a0: e8 7b f6 ff ff callq 495a20 <garbage_collect_with_gvl>
4963a5: 85 c0 test %eax,%eax
4963a7: 74 1f je 4963c8 <ruby_xcalloc+0x148>
4963a9: 48 89 de mov %rbx,%rsi
4963ac: bf 01 00 00 00 mov $0x1,%edi
4963b1: e8 8a 10 f8 ff callq 417440 <calloc@plt>
4963b6: 48 85 c0 test %rax,%rax
4963b9: 74 0d je 4963c8 <ruby_xcalloc+0x148>
4963bb: 48 89 c5 mov %rax,%rbp
4963be: e9 21 ff ff ff jmpq 4962e4 <ruby_xcalloc+0x64>
4963c3: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
4963c8: e8 23 a2 15 00 callq 5f05f0 <ruby_thread_has_gvl_p>
4963cd: 85 c0 test %eax,%eax
4963cf: 75 51 jne 496422 <ruby_xcalloc+0x1a2>
4963d1: e8 3a 2f 16 00 callq 5f9310 <ruby_native_thread_p>
4963d6: 85 c0 test %eax,%eax
4963d8: 74 43 je 49641d <ruby_xcalloc+0x19d>
4963da: 48 8d 3d 6f d2 ff ff lea -0x2d91(%rip),%rdi # 493650 <ruby_memerror_body>
4963e1: 31 f6 xor %esi,%esi
4963e3: e8 c8 e4 15 00 callq 5f48b0 <rb_thread_call_with_gvl>
4963e8: e9 f7 fe ff ff jmpq 4962e4 <ruby_xcalloc+0x64>
4963ed: e8 0e d5 ff ff callq 493900 <ruby_malloc_size_overflow>
4963f2: 48 8d 3d 27 1a 17 00 lea 0x171a27(%rip),%rdi # 607e20 <null_device+0x1378>
4963f9: e8 63 1b f8 ff callq 417f61 <negative_size_allocation_error.part.52>
4963fe: 48 8d 05 3b b4 44 00 lea 0x44b43b(%rip),%rax # 8e1840 <rb_eNoMemError>
496405: 48 8d 15 14 1a 17 00 lea 0x171a14(%rip),%rdx # 607e20 <null_device+0x1378>
49640c: 48 8d 35 94 b7 18 00 lea 0x18b794(%rip),%rsi # 621ba7 <yytranslate+0x807>
496413: 48 8b 38 mov (%rax),%rdi
496416: 31 c0 xor %eax,%eax
496418: e8 e3 77 fd ff callq 46dc00 <rb_raise>
49641d: e8 6a 1b f8 ff callq 417f8c <garbage_collect_with_gvl.part.65>
496422: e8 89 a2 ff ff callq 4906b0 <rb_memerror>
ん、思ったよりもでかいぞ?
static void * objspace_xcalloc(rb_objspace_t *objspace, size_t count, size_t elsize) { void *mem; size_t size; size = xmalloc2_size(count, elsize); size = objspace_malloc_prepare(objspace, size); TRY_WITH_GC(mem = calloc(1, size)); size = objspace_malloc_size(objspace, mem, size); objspace_malloc_increase(objspace, mem, size, 0, MEMOP_TYPE_MALLOC); return objspace_malloc_fixup(objspace, mem, size); }
ソースを見るに、前半はcallocするサイズの補正、後半はエラー処理系って感じですかね。納得したようなそれでもまだ多すぎるだろうというような気持ちになりつつ改めて逆アセンブルの中身を見ると、冒頭にdivがあります。そういえばこの連載ではdivが遅いのは前提としていましたがちゃんと見た方がいい気がしてきました。
機械語の速度はAgner FogさんのSoftware optimization resourcesにある"Instruction tables"を見るとよいです。以下の表はIvy Bridgeの場合の抜粋ですが、加算がめっちゃ速くて、除算がとても遅いことがわかりますね。(加算とか1クロックで3命令実行すごい)
| Instruction | Operands | Latency | Throughputの逆数 |
|---|---|---|---|
| ADD SUB | r,r/i | 1 | 0.33 |
| MUL IMUL | r64 | 3 | 1 |
| IMUL | r64,r64,i | 3 | 1 |
| DIV | r64 | 29-94 | 22-76 |
さて、このdivはruby_xmalloc2_sizeの中にあります。何をしているかというとオーバーフローチェックですね。正確にはC言語では符号無し整数はオーバーフローしないのでラップアラウンドチェックになります。
static inline size_t ruby_xmalloc2_size(const size_t count, const size_t elsize) { if (count > SIZE_MAX / elsize) { ruby_malloc_size_overflow(count, elsize); } return count * elsize; }
ところで、この種のチェックには二種類あります。チェックせずに実行すると例外が飛ぶゼロ除算エラーみたいなものと、今回のように単に意図と違う値が返ってくるものです。言い換えると、後者はとりあえず実行してからチェックしてもよいと言うことですね。チェックして違った場合、インテル® 64 アーキテクチャー および IA-32 アーキテクチャー 最適化リファレンス・マニュアルによると「分岐予測ミスには、約 20 サイクルのペナルティーが課せられる」のに加えて乗算のコストも無駄になるのですが、今回の場合どうせruby_malloc_size_overflow()なので気にしません。そのようにしてみたパッチと結果が以下の通りです。
diff --git a/include/ruby/ruby.h b/include/ruby/ruby.h index c41465a..4c50dd2 100644 --- a/include/ruby/ruby.h +++ b/include/ruby/ruby.h @@ -1608,13 +1608,15 @@ rb_num2char_inline(VALUE x) void *rb_alloc_tmp_buffer(volatile VALUE *store, long len) RUBY_ATTR_ALLOC_SIZE((2)); void rb_free_tmp_buffer(volatile VALUE *store); NORETURN(void ruby_malloc_size_overflow(size_t, size_t)); +#define UNLIKELY(x) (__builtin_expect(!!(x), 0)) static inline size_t ruby_xmalloc2_size(const size_t count, const size_t elsize) { - if (count > SIZE_MAX / elsize) { + size_t v = count * elsize; + if (UNLIKELY(count > SIZE_MAX / elsize)) { ruby_malloc_size_overflow(count, elsize); } - return count * elsize; + return v; } /* allocates _n_ bytes temporary buffer and stores VALUE including it * in _v_. _n_ may be evaluated twice. */
00000000004961b0 <ruby_xcalloc>: 4961b0: 48 8d 05 21 7b 44 00 lea 0x447b21(%rip),%rax # 8ddcd8 <ruby_current_vm> 4961b7: 41 54 push %r12 4961b9: 31 d2 xor %edx,%edx 4961bb: 55 push %rbp 4961bc: 53 push %rbx 4961bd: 48 89 fb mov %rdi,%rbx 4961c0: 48 0f af de imul %rsi,%rbx 4961c4: 48 8b 00 mov (%rax),%rax 4961c7: 4c 8b a0 30 06 00 00 mov 0x630(%rax),%r12 4961ce: 48 c7 c0 ff ff ff ff mov $0xffffffffffffffff,%rax 4961d5: 48 f7 f6 div %rsi 4961d8: 48 39 c7 cmp %rax,%rdi 4961db: 0f 87 d7 00 00 00 ja 4962b8 <ruby_xcalloc+0x108> 4961e1: 48 85 db test %rbx,%rbx 4961e4: 0f 88 96 00 00 00 js 496280 <ruby_xcalloc+0xd0> 4961ea: b8 01 00 00 00 mov $0x1,%eax 4961ef: 48 0f 44 d8 cmove %rax,%rbx 4961f3: 41 f6 44 24 10 40 testb $0x40,0x10(%r12) 4961f9: 75 45 jne 496240 <ruby_xcalloc+0x90> 4961fb: 48 89 de mov %rbx,%rsi 4961fe: bf 01 00 00 00 mov $0x1,%edi 496203: e8 38 12 f8 ff callq 417440 <calloc@plt> [...]
% sudo perf stat -d ./miniruby -e'GC.disable;i=1000000;while (i-=1)>0;Hash.new;end'
Performance counter stats for './miniruby -eGC.disable;i=1000000;while (i-=1)>0;Hash.new;end':
402.269177 task-clock (msec) # 0.992 CPUs utilized
23 context-switches # 0.057 K/sec
2 cpu-migrations # 0.005 K/sec
65554 page-faults # 0.163 M/sec
1164270325 cycles # 2.894 GHz [48.83%]
539587746 stalled-cycles-frontend # 46.35% frontend cycles idle [49.62%]
<not supported> stalled-cycles-backend
1660044950 instructions # 1.43 insns per cycle
# 0.33 stalled cycles per insn [63.91%]
330545296 branches # 821.702 M/sec [63.92%]
467450 branch-misses # 0.14% of all branches [64.22%]
<not supported> L1-dcache-loads:HG
9303493 L1-dcache-load-misses:HG # 0.00% of all L1-dcache hits [63.35%]
0 LLC-loads:HG # 0.000 K/sec [62.30%]
0 LLC-load-misses:HG # 0.00% of all LL-cache hits [48.70%]
0.405514667 seconds time elapsed
% sudo perf report --stdio
#
# Samples: 1K of event 'cycles'
# Event count (approx.): 1292902007
#
# Overhead Command Shared Object Symbol
# ........ ........ .................. ........................................
#
15.05% miniruby [kernel.kallsyms] [k] clear_page_c_e
8.95% miniruby miniruby [.] objspace_malloc_increase.isra.67
7.57% miniruby libc-2.19.so [.] _int_malloc
7.15% miniruby miniruby [.] vm_exec_core
4.26% miniruby libc-2.19.so [.] malloc
3.93% miniruby libc-2.19.so [.] __libc_calloc
3.44% miniruby [kernel.kallsyms] [k] async_page_fault
3.43% miniruby [kernel.kallsyms] [k] get_page_from_freelist
2.64% miniruby miniruby [.] vm_call0_body.constprop.120
2.37% miniruby miniruby [.] ruby_xcalloc
2.17% miniruby miniruby [.] vm_call_cfunc
1.87% miniruby miniruby [.] rb_call0
1.50% miniruby miniruby [.] rb_callable_method_entry
0.46 → 0.40 なので結構変わりましたね。アセンブラ上ではimul → mov mov mov → div となっていますが、imulとmov mov movが同時に走って、divしながら投機的実行でZFを使うtestb直前まで走っちゃってるんじゃないでしょうか、たぶん。UNLIKELYなしだとそこまで性能向上無かったので、多分他の場所でもっと効いてるんだと思います。
最初はもうちょっと違う話を書くつもりだったんですが長くなったので今日はここまで