Waylandで日本語入力への道: Return of RVO編

adventar.org

Kernel/VM Advent Calendarの何日目かの記事です.

前回のあらすじ: std::string(C++)のことを調べて, Rustで作れるようになった. でも, 小さい文字列用に最適化された状態にはできない… ちくしょう! アセンブラに相談だ!

DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.

文字列をやってるとだいぶ日本語入力に近付いてきた気持ちになるこのシリーズいかがおすごしでしょうか.

RVO?なんですか?

さて前回は, std::string(C++)の小さい文字列用フォーマットを作りたかったものの挫折したわけですが…. このへんをいろいろ調べていくと, Return Value Optimization (RVO)による現象なのかなあとわかってきました.

cpprefjp.github.io


せっかくなので, このへん深掘りしておきましょう. 小さいC++プログラムを作ってやってみましょう. メインはこんな感じ.

#include <string>
#include <iostream>

extern "C" {
	std::string f();
}

int main() {
	std::string s = f();
	std::cout << s << std::endl;
	printf("s in main() is at %p\n", &s);
	printf("  buffer address: %p\n", *(void**)&s);

	return 0;
}

これに対して, C++によるf()の実装とRustによるf()の実装を作ります. C++版はこうなります.

extern "C" std::string f() {
	std::string foo = "";
	printf("foo in f() is at %p\n", &foo);
	printf("  buffer address: %p\n", *(void**)&foo);
	return foo;
}

RustでSSOしないと, こんな感じ

#[no_mangle]
pub unsafe extern "C" fn f() -> CxxString {
    let buffer: Box<[u8]> = Box::new([0; 32]);
    let ptr = Box::into_raw(buffer) as *const _;
    println!("string buffer is at {:?}", ptr);
    CxxString {
        ptr: ptr,
        size: 0,
        capacity: 32,
        pad: [0; 8],
    }
}

で, これらのアセンブラを見ます. まずはmain()から.

    11b6:       48 89 e5                mov    %rsp,%rbp
...
        std::string s = f();
    11cd:       48 8d 45 c0             lea    -0x40(%rbp),%rax
    11d1:       48 89 c7                mov    %rax,%rdi
    11d4:       e8 77 fe ff ff          call   1050 <f@plt>

ここはf()を呼んでいるところですが, %rdiレジスタに"-0x40(%rbp)"が入っています. ここで%rbp=%rspなので, まあスタック上のアドレス(std::string sのアドレス)が, f()への隠し引数として渡されているわけです.

C++版とRust版では, この引数の扱いが変わってきます. C++版のf()を見ましょう.

    22de:       48 89 7d d8             mov    %rdi,-0x28(%rbp)        # -0x28(%rbp) = %rdi = 隠し引数 = "mainのs"
...
    2301:       48 8b 45 d8             mov    -0x28(%rbp),%rax
    2305:       48 8d 0d f4 0c 00 00    lea    0xcf4(%rip),%rcx        # 3000 <_fini+0x928>
    230c:       48 89 ce                mov    %rcx,%rsi               # 多分文字列 ""
    230f:       48 89 c7                mov    %rax,%rdi               # callの第一引数: "mainのs"
    2312:       e8 69 fe ff ff          call   2180 <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC1IS3_EEPKcRKS3_@plt>
    # call されるのはstd::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)

ここでcallされているのは, std::stringのコンストラクタです. 結局, main内の"std::string s"に対して, f()の中から直接, 文字列("")を引数にとるコンストラクタを呼んでいます.

では, Rust版ではどうなっているのでしょう.

    7847:       48 89 7c 24 08          mov    %rdi,0x8(%rsp)
...
    78c6:       e8 85 f9 ff ff          call   7250 <_ZN5alloc5boxed16Box$LT$T$C$A$GT$8into_raw17h9f973d5138f7f2c2E>
    78cb:       48 89 44 24 48          mov    %rax,0x48(%rsp)         # 0x48(%rsp) = Box::into_raw()
...
    7951:       48 8b 4c 24 08          mov    0x8(%rsp),%rcx
    7956:       48 8b 44 24 10          mov    0x10(%rsp),%rax
    CxxString {
        ptr: ptr,
    # ここから返り値へのコピー
    795b:       48 8b 54 24 48          mov    0x48(%rsp),%rdx         # %rdx = 0x48(%rsp) = Box::into_raw()
        size: 0,
        capacity: 32,
        pad: [0; 8],
    7960:       48 c7 84 24 90 00 00    movq   $0x0,0x90(%rsp)
    7967:       00 00 00 00 00 
    CxxString {
    796c:       48 89 11                mov    %rdx,(%rcx)             # ptr = %rdx = 0x48(%rsp) = Box::into_raw()
    796f:       48 c7 41 08 00 00 00    movq   $0x0,0x8(%rcx)          # size = 0
    7976:       00 
    7977:       48 c7 41 10 20 00 00    movq   $0x20,0x10(%rcx)        # capacity = 32
    797e:       00 
    797f:       48 8b 94 24 90 00 00    mov    0x90(%rsp),%rdx
    7986:       00 
    7987:       48 89 51 18             mov    %rdx,0x18(%rcx)         # pad

Rust版のf()でも, 返り値となる構造体にBoxで確保したバッファやcapacityなどを直接コピーしています. ここで最適化されて直接構築されるのがある意味で問題で, たとえば一時オブジェクトがポインタで返っていってそれが呼び出し側でコピーコンストクタなり呼ばれなおす…という感じならもしかしてうまくいっていたのかもしれません.

結局のところ, 隠し引数のアドレスをとってきて, そのアドレスを元に適切なデータを構築する必要があります. ということは, このrdiレジスタに来る隠し引数をとる方法がなにかあれば…なにか…なにかないのか……?

asm!()でい!

レジスタには来てるんだから, asmでとればいいじゃないという話ですね. ということで, こんな感じ

    let rdi: usize;
    unsafe { asm!("mov {}, rdi", out(reg) rdi) };
    eprintln!("rdi = {:#x}", rdi);
    CxxString {
        ptr: (rdi + 16) as *const _,
        size: 3,
        capacity: 0x6f6f66, // "foo"
        pad: [0; 8],
    }

asm!()でレジスタrdiの値をとって, そこから最適化状態のstd::stringのバッファ位置を計算します. capacityのところに, 文字列になるように値を設定すれば…

rdi = 0x7ffd92b51c40
foo
s in main() is at 0x7ffd92b51c40
  buffer address: 0x7ffd92b51c50

やったね, 動きました.

ところで, 返り値の書きこみ先がわかったなら, もっと直接的に書く感じのコードでもいいのでは? たとえばこんな

#[repr(C)]
struct CxxShortString {
    ptr: *const u8,
    size: u64,
    buffer: [u8; 16],
}

#[no_mangle]
pub unsafe extern "C" fn f() {
    let rdi: *mut CxxShortString;
    unsafe { asm!("mov {}, rdi", out(reg) rdi) };
    eprintln!("rdi = {:?}", rdi);
    let s = "foo";
    (*rdi).ptr = &(*rdi).buffer as *const _;
    (*rdi).size = 3;
    (*rdi).buffer[..s.len()].copy_from_slice(s.as_bytes());
}

これもいい感じに動きました. どっちがRustっぽいでしょうね. asm!()な時点で終わりっちゃ終わりか.

asm!()を使ったりバイナリ形式を仮定したり, ひたすらプラットフォーム依存な感じですが, そういうとこ遊べるのも自作IMEのいいところということでここはひとつ. これで最適化されたstd::string(C++)を作れるようになりました. しかし, これ使いづらいなあ…

ということで, 貯まってたネタがなくなってきたので, 次回はどうだろう…多分

次回: Waylandで日本語入力への道: Preeditを作ろう編

かなあ…