こういうツイートを見た。気になったので試してみたら本当にそうらしい。
x64でEAXに値を入れるとRAXの上位は(符号拡張ではなく)0になる。なのでC/C++で配列の添え字に32bit整数を使うと符号拡張命令が追加されて遅くなる。LP64環境で配列の添え字にintを使うとそこそこ性能に影響を与える。配列の添え字になり得る整数は理由がなければsize_tを使うと良い。はず。
— takl (@takl) 2020年2月3日
EAXとかRAXとかいうのはレジスタの名前だ。特にRAXは64ビットのレジスタで、その下位32ビットにEAXとしてアクセスできる。
.-- RAX ------------. | .----EAX---. | | .--AX-. | | |AH|AL| 64 32 16 8 bit
EAXに値をmovすると、上の32bitはゼロになる。だが、ゼロになると困る場合がある。32bitの符号付き整数を64bitに拡張して使いたいとしよう。
EAXに負の符号付き整数を入れた場合、例えば-1は0xFFFFFFFF
だが、これを64bitレジスタに入れて上位ビットが0にされると0x0000'0000'FFFF'FFFF
になってしまう。
すると今度は負の数でなく、2^32-1
という値になってしまい、意味が変わってしまう。
そのため、CPUには符号を考慮してキャストするための命令が用意されており、この機能は符号拡張と呼ばれている。
int
は多くの環境で32ビットであり、64ビット環境だとポインタが64ビットなので、配列の添字としてint
を使おうとすると符号拡張命令が入って遅くなるということらしい。
確認してみよう。
これが、
#include <vector> #include <cstdint> int nth_int(const std::vector<int>& v, const std::int32_t n) noexcept { return v[n]; } int nth_uint(const std::vector<int>& v, const std::uint32_t n) noexcept { return v[n]; } int nth_size_t(const std::vector<int>& v, const std::size_t n) noexcept { return v[n]; } int nth_uint64(const std::vector<int>& v, const std::uint64_t n) noexcept { return v[n]; } int nth_int64(const std::vector<int>& v, const std::int64_t n) noexcept { return v[n]; }
こうなった。
nth_int(std::vector<int, std::allocator<int> > const&, int): mov rax, QWORD PTR [rdi] movsx rsi, esi mov eax, DWORD PTR [rax+rsi*4] ret nth_uint(std::vector<int, std::allocator<int> > const&, unsigned int): mov rax, QWORD PTR [rdi] mov esi, esi mov eax, DWORD PTR [rax+rsi*4] ret nth_size_t(std::vector<int, std::allocator<int> > const&, unsigned long): mov rax, QWORD PTR [rdi] mov eax, DWORD PTR [rax+rsi*4] ret nth_uint64(std::vector<int, std::allocator<int> > const&, unsigned long): mov rax, QWORD PTR [rdi] mov eax, DWORD PTR [rax+rsi*4] ret nth_int64(std::vector<int, std::allocator<int> > const&, long): mov rax, QWORD PTR [rdi] mov eax, DWORD PTR [rax+rsi*4] ret
確かに、int32_t
を渡した時はmovsx
(move sign extension)が呼ばれているし、そもそもesi
からrsi
へmov
していて無駄に見える。64ビット整数版より命令が一つ多い。
少しよくわからないのがuint32_t
を渡した時で、符号なしなのでゼロで拡張してくれて良いのでは?と思うのだが、mov esi esi
という謎の命令が入っている。なにこれ?意味なくね?
size_t
は期待通り、直接アドレス操作に使われている。64bitの整数も同じくだ。
私はもとよりsize_t
を使っているのだが、添字をキャッシュしておくようなコードを書いている時に32bitで足りるんだからメモリアクセスの効率を考えると32bitにした方がいいのかなあと考えて試したことがあった。
だがその時あまり速くならず、理由がよくわからないままそのブランチは切り捨てたのだが、もしかしたらここで追加されている命令のせいかもしれない。
おそらくその時足されていたであろう、uint32_t
で追加される同じものを同じ所に代入している命令の意味はわからないが……。
ところで、私の知っているいくつかのFortran製のプロダクトでは皆単にinteger
を使っていて、64ビットにしていることはほぼ無い。なのでこれは結構インパクトがあるのではと思って適当なコードを書いてみたら、なんだかよくわからなくなっていた。
なぜかは全くわからんが、命令数が異様に多い。C++版が2,3命令のところで9命令もある。-Ofast
にしても変わらなかった。読み解くのも面倒になったので貼るだけにしておこう。あとで気が向いたら読む。
(追記):読みました。