kubo39's blog

ただの雑記です。

LDC向けのSIMDライブラリを作っているけど、いろいろつらい

x86intrin

勉強がてらにLDCでx86intrinsicっぽくSIMDを書けるライブラリを書いてみている。まだまだ全然途中なので使わないでください。

github.com

_m128iとかじゃなくてそのままbyte16とかを直接 _mm_XXX の引数にとったりするようにしてるけど、将来的にはたぶん変えます。

そういう作業をしてる中でいくつか問題にあたったので、書き残しておきます。

LDCでunittestブロックとDFLAGSの指定を共存できない

そういう問題があるので dub test --compiler だと DFLAGS="-mattr=+sse4.2 つけてもsse4.2環境のテストができない。別にスクリプトを作ってそっちを実行するようにして回避。

intrinsicがundefined reference

clangやrustと同じバージョンのLLVMを使っているけどLDCだけundefined referenceといわれるintrinsicがあったりする。cvt系とか全部死亡です、ありがとうございました。

Comment out convert intrinsics since undefined reference error · kubo39/x86intrin@4c831f9 · GitHub

普通の関数がundefined reference

sse2でintrinsic使ってない関数を呼び出そうとしたらundefined referenceで無事死亡。

おわりに

LDCSIMDけっこうつらいのではないか

perfを使ってD言語のプロファイリング結果をみる

まず手元の /proc/sys/kernel/perf_event_paranoid を確認する。Ubuntuとかだと最弱設定になってるので変えとく。

$ cat /proc/sys/kernel/perf_event_paranoid
3
$ sudo sh -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid'

普通に perf record にかける。

$ perf record dmd fib.d
[ perf record: Woken up 1 times to write data ]
[kernel.kallsyms] with build id ef8b1ed123757213c70bca103ce70b59825d9c11 not found, continuing without symbols
[ perf record: Captured and wrote 0.052 MB perf.data (1240 samples) ]

perf report で出力するときに ddemangle に食わせるとsymbol nameがdemangleされるので便利。

$ perf report| ddemangle| head -20
[kernel.kallsyms] with build id ef8b1ed123757213c70bca103ce70b59825d9c11 not found, continuing without symbols
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 1K of event 'cycles:uppp'
# Event count (approx.): 706573056
#
# Overhead  Command   Shared Object            Symbol                                                                                                                                                                                                                                                
# ........  ........  .......................  ......................................................................................................................................................................................................................................................
#
    12.51%  ld        libbfd-2.30-system.so    [.] bfd_hash_lookup
     7.98%  ld        libc-2.27.so             [.] __gconv_transform_utf8_internal
     5.34%  dmd       dmd                      [.] void dmd.lexer.Lexer.scan(dmd.tokens.Token*)
     3.80%  ld        libc-2.27.so             [.] __strcmp_sse2_unaligned
     3.50%  ld        libc-2.27.so             [.] __mbsrtowcs_l
     3.32%  ld        x86_64-linux-gnu-ld.bfd  [.] walk_wild_section_general
     3.31%  ld        libc-2.27.so             [.] internal_fnwmatch
     3.25%  ld        x86_64-linux-gnu-ld.bfd  [.] walk_wild_section_specs3_wild2
     2.81%  ld        libc-2.27.so             [.] __strnlen_avx2

まあここまでは別によい。

dmd -profile と perf(oprofile) の使い分けってどうなの?みたいなの聞かれたときに dmd -profile はLinuxだとrbtsc命令使っててコア間同期がうんたら〜みたいな話をしてしまって、まあそれは間違いじゃないのだけれど、実際そもそもdmd -profileはtracing方式(すべてのDの関数コールにフックする)のに対しperfはsampling方式(タイマーの間隔を決めて現在のスタックの状態を収集してどの関数が統計的に多くよばれてるか)という違いがあり、用途によって使い分けられるという話をすべきだったなー、と思った。(長い)

でも大体のケースはperfでいいんじゃないのかな、みたいな。特にsampling方式だと実際に負荷がかかってる本番環境とかでも計測できるのが嬉しい。実はこのパスがめっちゃ呼ばれてる、みたいなのとかは開発環境じゃわからなかったりするので。

D言語で一ヶ月前を表すとき

日付の扱いは忘れそうなのでメモ。

一ヶ月前を表す場合、可能であれば前月の同日同時刻を表し、前月に同日が存在しない場合は差分を計算して付け足す。

import std.datetime;
import std.stdio;

void main()
{
    auto currTime = Clock.currTime(UTC());
    writeln(currTime);                              //  2018-Sep-30 20:12:00.0198441Z
    writeln(currTime.add!"months"(-1));  // 2018-Aug-30 20:12:00.0198441Z
    currTime = Clock.currTime(UTC());
    writeln(currTime.add!"months"(1));  // 2018-Oct-30 20:12:00.0199153Z

    auto date = Date(2018, 7, 31);
    writeln(date.add!"months"(-1));  // 2018-Jul-01
    date = Date(2018, 7, 30);
    writeln(date.add!"months"(-1));  // 2018-Jun-30
}

なので3月とかはこうなる。

import std.datetime;
import std.stdio;

void main()
{
    auto date = Date(2018, 3, 31);
    writeln(date.add!"months"(-1));  // 2018-Mar-03
    date = Date(2018, 3, 29);
    writeln(date.add!"months"(-1));  // 2018-Mar-01

    // うるう年は考慮される
    date = Date(2016, 3, 29);
    writeln(date.add!"months"(-1));  // 2016-Feb-29
}

D言語のprecise(正確な) GC

(追記: 編集あり)

実は結構前に保守的なGCから正確なGCになって いた いなかった。仕組みはマーク・アンド・スィープで変わらないが性能は大幅に向上が期待できそう。

github.com

これによって、

  • メモリ確保時
    • allocatorの使い分けによる効率的なメモリ管理(それぞれのアロケータの特性が活かせる)、これは保守的GCでは安全側に倒さざるをえなかったので最大のメリットになる
    • スレッドローカルキャッシュを使うことでグローバルなGCロックを避けられる
  • ゴミ集め時
    • 並列にスキャンするようになったのでパフォーマンスが向上
    • 特定の条件を満たしていればストップ・ザ・ワールド(STW)が発生しなくなる
      • 例えば新たに追加された型レベルの仕組み isolated によって静的にスレッドローカルなヒープのみを使っているかを判定(isolatedはスレッドローカルなヒープにつく型といえる)でき、isloatedしかないと判定したときはSTWしない

わりとGoに近い感じで、アロケータの柔軟性でGoより優れていて、インクリメンタルGCによるレイテンシの仕組みはシステムプログラミング言語という特性上入れなかったという感じか。 さすがにJVMに比べると厳しいが、これまでの保守的GCに比べるとずいぶんよくなっている、はず。

(追記ここから)

まだマージされてなかった。。すまんな、、

OpenSUSEを試してみてる

デスクトップ環境として最新のLeap使ってみてる。 感想としては、

  • IM(Mozx <-> US)の切り替えがめちゃくちゃ遅い。普通にタイピングに支障が出るレベル。
    • 方向関係なく、「hankakuzenkaku」で切り替えると遅く、「hankakuzenkaku+Shift」だと気にならない程度の遅さになる。追ってみてるがよくわからん。
  • キーボードレイアウトの切り替えが何故か反映されない?setxkbmapでは変更できずYaSTを使って一時的にできたと思ったがいつのまにかまたUS配列に戻っていた。
  • 「設定」とYaSTの切り分けが意味不明。設定の「デバイス」->「キーボード」でbindingとかを変更するようになっているのにキーボードレイアウトはYaSTとか意味がわからん。
  • GNOMEデフォルトのキーバインドの操作性がいまいち
    • これは慣れもある気がするけどなんとなく使いづらい感じはしてる、こっちもi3wmにしようかどうか

総じてクソと言わざるをえない、なんのためにわざわざLeap選んだと思ってんだよ。こういう雑事に悩まされないための初心者向けのやつなんじゃねえのか。 Ubuntuもいろいろ不満に思うことはあるけど、完成度は天地ほどの差がある。

まあ単に慣れてない、わかってないだけならいいんだけど、まさかこのレベルではまるとはなあ。。。とりあえず初心者にはおすすめできないですね。

プロセス起動でposix_spawnとかvforkとかを使うはなし

posix_spawn/vfork/clone(CLONE_VFORK) はメモリを共有するので速い. 速いが、親子でメモリを共有するので危険である. それでも速いのでプロセス起動で気をつけて使っている言語があり、どういう実装をしているか調べた.

前提

Rust

posix_spawnを使う条件

  • ENOENTを直接返すこと
    • 単にposix_spawn側の実装バグの問題
  • Linux/MacOS/FreeBSDであること
    • ENOENTを直接返すことができるOSのため
  • Linuxの場合, Glibcのバージョンが2.24以上であること
    • ENOENTを返すposix_spawnはこのバージョンから
  • getcwdでディレクトリを返すこと
  • getgidが成功する
  • getuidが成功する
  • PATHが変更されていないこと
    • race conditionがある

どう使っているか

  • posix_spawnに持たせる属性を決めている
  • posix_spawnattr_setsigmask シグナルをマスクしている
    • forkするスレッドはfork-exec間でシグナルを受け付けたくない
  • posix_spawnattr_setsigdefault SIGPIPEをSIG_DFLにしている
    • シグナルハンドラでグローバル変数を参照するかもしれないので避けたい
    • 別スレッドがシグナルをうけたときにメモリを書き換えるかも、という危険があるので
    • (追記): そういえばRustはランタイム初期化時にSIGPIPEをSIG_IGNにセットしているので、その設定によって起動するプロセスが影響を受けることを避けたいためにSIG_DFLにしなおしているというほうがメインな気がする
  • (追記): glibc2.29+だとchdirも指定できるようになっている

まとめ

Dellの中の人でFreeBSDのコアコミッタの人なので、けっこう信頼できるんじゃないだろうか. ただいくつか気になる点はあり、

  • すべてのシグナルに対してSIG_DFLを定義しなおさなくてもいいのか
    • してたわ
  • uid/gidが特権ユーザでないことはチェックしなくてよいのか
    • これはposix_spawn側で対策されていそうな雰囲気がある
  • PATHの変更をみていること
    • PATHは状態変わることで振るまいは変わるが具体的にどういった問題があるんだろう
    • race conditionがある

(追記) あとでいろいろ調べ直したけどめちゃくちゃよく考えられてる実装だった。

Go

clone(CLONE_VM | CLONE_VFORK)を使う条件

どう使っているか

  • clone(2)に SIGCHLD|CLONE_VFORK|CLONE_VM を指定して使っている
  • cloneでシステムコール呼び出しをしている場合, 親子はスタックを共有しないらしい
    • 引数にmmap()で確保したメモリを渡すことで制御できるようだ

fork-exec間に子プロセスは何をしているか

  • keep capabilitiesをprctlでセットしている(実行ユーザとかが変更されたかチェック)
  • ほかにもいろいろやっているがforkと共通なので割愛
    • 主にcapabilitiesとかなので権限まわりをセキュアにしたいのだろう

まとめ

自前でがんばっているためコードはかなり複雑になる. 現時点では制限がかなり厳しい(Linuxかつamd64のみ)ので、逆に意図しない問題は起きづらそう. 当然ゴルーチンが全部停止するためスループットが悪化するという懸念はされている.

Ruby

vforkを使う条件

  • vforkを実装していること
  • uid/gidが特権ユーザでないこと
    • setuidとかされたくない

fork-exec間に子プロセスは何をしているか

  • シグナルに対してSIG_DFLを再定義
    • invalidなsignumは無視
    • SIG_IGN が定義されてたら SIG_IGN
      • その場合でもSIGPIPEはSIG_DFLにする
  • sigprocmaskでシグナルをmaskする
    • pthread_sigmaskでないのはasync signal safeなものを使いたいため
  • pthread_setcancelstatePTHREAD_CANCEL_DISABLE して cancel が無効であることを宣言する
    • pthread_cleanup_push で登録した cancellation cleanup handler も呼び出されるのは困る
    • シグナルハンドラと同様の理由

まとめ

だいぶ安全よりに倒しているのではないか.

Nim

posix_spawnを使う条件

  • posix_spawnが存在していてLinuxでないこと
    • Linuxだとclone(2)を使う
    • いろいろよくわからない条件指定がある
    • Rustと違ってglibcのバージョンみないので存在しないコマンドにたいしてENOENTが返ってこない可能性がある

cloneを使う条件

  • Linuxだったら基本使う
    • fork-exec間はasync-signal-safeな関数しか使わないようになっている

どう使っているか

  • 基本Rustと一緒だけどシグナルまわりが怪しい
    • 具体的にいうとSIGPIPEをSIG_DFLにセットしていないとかが違う
    • 追記:これはRustがランタイム起動時にSIGPIPEをSIG_IGNにセットする特殊事情なので特におかしいことではない
  • clone()を使っているときに親がchdirしてからclone()を呼び、また親のディレクトリを戻している?
    • chdirしてからcloneが返ってくるまで親のカレントディレクトリが変わるというわりとめちゃくちゃな実装

まとめ

基本的に優良な実装にみえる. ただしいろいろ不明すぎて安全に使えるのか不安. シグナルまわりとか.

clone()を使っていてGoっぽいけどカレントディレクトリ指定とかやたら怪しい実装になっている

D言語の八進数リテラルの特殊扱い

D言語はかつて八進数リテラルを定義していたが、現在は仕様上Invalidとなっており std.conv.octal を使うようにエラーメッセージが出現する。

(dmd-2.081.1)$ cat octal.d
void main()
{
    auto _ = 010;
}
(dmd-2.081.1)$ dmd octal.d
octal.d(3): Error: octal literals 010 are no longer supported, use std.conv.octal!10 instead

ところが 00 ~ 07 の八進数リテラルのみ特殊扱いしており、以下のコードは評価可能であり実行もされる。 (実はある時点で 0809 というそもそも不正なリテラルをうけつけるバグが混入してしまったのだが、これは修正しておいたので2.082.0以降では正しくエラーとなる)

void main()
{
    assert(01 == 1);
}

これは特殊扱いというより過去そのまま残ったバグなのであるが、phobosをはじめ2文字で表す八進数リテラルは広く使われているために今後修正は(おそらく)行われないだろう。

つまりD言語には隠れた仕様上不正だが許容されている構文が存在しているということである。