kubo39's blog

ただの雑記です。

D言語のftoa/dtoa実装

以前のポストでftoa実装がナイーブな感じであるという話はしたが、説明はしてなかったので残しておく。

アルゴリズム

D言語浮動小数点数の10進表記アルゴリズムは指数部の数値によって3種類のアルゴリズムを使い分けている。

  • Algorithm A: 指数部が大きい場合 (exp >= T.mant_dig)
    • (例: doubleの場合) exp >= 53 のとき
  • Algorithm B: 指数部が小さい場合 (exp < T.mant_dig - 61)
    • (例: doubleの場合) exp < -8 のとき
  • Algorithm C: 指数部が0に近い場合
    • 上記の中間の数値

ここの指数部の値はIEEE754におけるバイアスなどを引いた後の値であることに注意する。

Algorithm A

表示される数値: 仮数部のあとに0がいくつも続くような見た目になる。

この場合は少数部がないので、単純に整数部分を10で割っていく。 整数部分をulongだけで表現できない場合のためにBigIntと同じようにulongの配列を使う。 その際は割り算の計算の単純化と高速化のためにulongのうち60bitのみを使う。

割り算は整数の割り算を使い、計算結果の上位4ビットがキャリーとして使われる。

Algorithm B

表示される数値: 0とドットのあとに複数の0が続き、そのあと仮数部が表示される。

この場合は整数パートが存在しない。 このアルゴリズムでは10をどんどん掛けていって、整数部分を取り除く。 このアルゴリズムでも数値はulongの配列として保持し、ulongのうち60bitしか使用しない。

乗算では普通の整数乗算を利用し、その結果最上位4ビットに桁が入ることがある。 この4ビットはキャリーとして次の乗算結果に加算される。

Algorithm C

この場合は整数パートと少数部が存在し、それぞれ1個のulongで表現できる。 そのためこれが一番高速になる。

仮数部は両方のパートか少数部に出現する。

最初に整数部を10で割っていく。次に少数部に10を掛けていく。 これを表示上必要な桁に達するまで行う。 一番最後に表示される次の桁の数値をみて丸目モードを決定する。

D言語で使われているデータ構造・アルゴリズム

atof/ftoa相当や正規表現まわりのコードをざっくり読んでて、phobosとかで使われてるアルゴリズムってどんなのがあるっけというのをまとめてみた。主に、というかだいたい標準ライブラリで超雑なまとめです。

std.algorithm

  • Boyer-Moore法(std.algorithm.searching.boyerMooreFinder)
  • IntroSort(std.algorithm.sorting.sort!unstable)
  • TimSort(std.algorithm.sorting.sort!stable)
  • レーヴェンシュタイン距離(std.algorithm.comparison.levenshteinDistance)

std.bigint

  • Karatsuba法(乗算)

std.container

  • 赤黒木(std.container.rbtree)

std.conv

  • naive float parsing algorithm (std.conv.parse)

上のアルゴリズムをざっくり解読したもの: https://gist.github.com/kubo39/3aa53f0661dd645f1f0f7d3fef62b5c6

std.format

  • naive ftoa algorithm (std.format.internal.floats)

ナイーブな処理とか、他の言語で使われてるアルゴリズムとか:

kubo39.hatenablog.com

std.mathspecial

  • ガンマ関数(std.mathspecial.gamma)

std.random

std.regex

後半のほうで少しDのregexの処理を追っている: https://gist.github.com/kubo39/78eb0fbc770aeeba5f7c7126b5a85214

core.bitop

  • 分割統治法(core.bitop.popcnt)

WSL2上でRustでespのstdをやっていく予定のはなし (1)

Interface誌のRust特集で、espのstdをやっていくぜ!となったが悪戦苦闘している。

まずドライバを入れてWindows側でデバイスを認識するところからつまづいた。

Establish Serial Connection with ESP32 - ESP32 - — ESP-IDF Programming Guide latest documentation

このへんでドライバを入れていたが、デバイスを接続してデバイスマネージャーをみてもCOM portsが生えない。

ひたすら詰まってTwitterで愚痴っていたらnakabayashi先生に助けてもらった。

というわけでなんとかWindows側で認識するところまでいき、WSL側にattachするところまではMSの公式記事の通りにやればいった。

Connect USB devices | Microsoft Learn

あとはexampleを動かすだけだと思ったら、今度はここで詰まった。

interface202305-c3-std-rust/hello_c3$ cargo espflash --release --monitor
New version of cargo-espflash is available: v2.0.0-rc.3

✔ Use serial port '/dev/ttyACM0'? · yes
Serial port: /dev/ttyACM0
Connecting...

Error: espflash::serial_error

  × Failed to open serial port /dev/ttyACM0
  ├─▶ Error while connecting to device
  ├─▶ IO error while using serial port: Permission denied
  ╰─▶ Permission denied

WSL側のカーネルのビルドが必要か?と思ったが、configですでに有効になっているはず。

$ zcat /proc/config.gz | grep CP210
CONFIG_USB_SERIAL_CP210X=y

てことはたんに権限まわりっぽいけどよくわからん。

isInstanceOfテンプレートとalias templates

Slackで以下のコードが通らないのはなぜ?という話があった。

import std;

static assert(isInstanceOf!(Array, Array!char));  // OK!
static assert(isInstanceOf!(Regex, Regex!char));  // Compile Error!

どうもこれを調べてみると、alias templatesの場合だとうまくいかないらしい。

public alias Regex(Char) = std.regex.internal.ir.Regex!(Char);

isInstanceOfは内部的にis式を用いているので、こういったことになっている。

struct S1(Char) {}
alias S2(Char) = S1!Char;

static if (is(S1!char == T3!Args3, alias T3, Args3...))
{
    static assert(__traits(isSame, T3, S1));
    pragma(msg, T3);  // S1(Char)
    static assert(__traits(isTemplate, S1));
    static assert(__traits(isTemplate, T3));
    pragma(msg, __traits(identifier, T3));  // S1
    pragma(msg, T3!char);  // S1!char
    static assert(is(T3!char == S1!char));
    static assert(is(Args3[0] == char));
}

static if (is(S2!char == T4!Args4, alias T4, Args4...))
{
    static assert(!__traits(isSame, T4, S2));
    pragma(msg, T4);  // S1(Char)
    static assert(__traits(isTemplate, S2));
    static assert(__traits(isTemplate, T4));
    pragma(msg, __traits(identifier, T4));  // S1: コピペミスではなく、alias templatesの別名参照先の方を指している
    pragma(msg, T4!char);  // S1!char
    static assert(is(T4!char == S2!char));
    static assert(is(Args4[0] == char));
}

現状の言語機能では型のaliasを取るtraitsはあるが、templateのaliasを取る方法は提供されていない。 また、これは言語機能として提供しないと(自分が知る限りでは)取得することはできない。

最初に戻ると、isInstanceOfの実装としてaliasを考慮しないようになっているのはどうなんだ?という気もしてくる。

Nimと名前修飾の小ネタ

NimはDWARF v5の時点では DW_AT_language の割当はないけど、ソースコードはDWARFに入れることが できて $ eu-readelf --debug-dump=info hello とかで確認できる。

proc main() =
    echo "Hello!"

main()
$ nim c --debugger:native hello.nim
(...)
$ file hello
hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=db5c80607aaed2ec154efb5863143879931d59c6, for GNU/Linux 3.2.0, with debug_info, not stripped
$ eu-readelf --debug-dump=info hello
(なんかいっぱい出る)

ただしdemanglerはないのでどうするかというと、 --debugger:native を指定したときに生成される hoge.ndiというファイルに名前修飾された変数・プロシージャとソースコードの対応がある。

$ find ~/.cache/nim/hello_d/ -name "*.ndi" # ~/.cache の下になんか生えてる
/home/kubo39/.cache/nim/hello_d/stacktraces.ndi
/home/kubo39/.cache/nim/hello_d/dragonbox.ndi
/home/kubo39/.cache/nim/hello_d/formatfloat.ndi
/home/kubo39/.cache/nim/hello_d/ansi_c.ndi
/home/kubo39/.cache/nim/hello_d/memory.ndi
/home/kubo39/.cache/nim/hello_d/assertions.ndi
/home/kubo39/.cache/nim/hello_d/dollars.ndi
/home/kubo39/.cache/nim/hello_d/since.ndi
/home/kubo39/.cache/nim/hello_d/miscdollars.ndi
/home/kubo39/.cache/nim/hello_d/io.ndi
/home/kubo39/.cache/nim/hello_d/schubfach.ndi
/home/kubo39/.cache/nim/hello_d/countbits_impl.ndi
/home/kubo39/.cache/nim/hello_d/system.ndi
/home/kubo39/.cache/nim/hello_d/coro_detection.ndi
/home/kubo39/.cache/nim/hello_d/bitops_utils.ndi
/home/kubo39/.cache/nim/hello_d/widestrs.ndi
/home/kubo39/.cache/nim/hello_d/digitsutils.ndi
/home/kubo39/.cache/nim/hello_d/iterators.ndi
/home/kubo39/.cache/nim/hello_d/hello.ndi

中身はこんな感じのテキストデータになっている。

$ head -5 ~/.cache/nim/hello_d/hello.ndi 
main    main__hello_1   /home/kubo39/dev/nim/hello.nim  1       5
args    args    /home/kubo39/.choosenim/toolchains/nim-1.6.8/lib/system/io.nim  804     19
locals  locals  /home/kubo39/.choosenim/toolchains/nim-1.6.8/lib/system.nim     2209    29

ndiは https://github.com/nim-lang/Nim/blob/7f6e800cafc7b73625893fb5280eb8b51a15b252/compiler/ndi.nim をみると Nim Debug Info の略っぽい。

ただし、こいつは別にnim-gdbで対応しているわけでもないし、nmやc++filtに喰わせればいい感じにしてくれるというフォーマットでもない。 そのため現状では対応したければ自分で頑張る必要がある。

一応Callgrind対応にかんしては https://github.com/kraptor/nim_callgrind というpythonスクリプトを提供している人もいる。

jemallocator/tikv-jemallocatorのdisable_initial_exec_tlsってなに?

GitHubでよくjemallocatorを使っているときにdisable_initial_exec_tls featureを有効にしているのをみかける。

これはなにかという話をする前に、TLSモデルについて知っておく必要がある。

TLSにはアクセスモデルというものがあり、モジュールの種類(実行バイナリであるとかdlopenでロードされるものであるとか)や 別のモジュールからアクセスされうるものかといった条件によって種類がある。

ELFなどで一般に用いられているものとして local-exec initial-exec local-dynamic global-dynamic の4種類がある。

ざっくり、

  • local-exec: 実行バイナリの中でのみ参照されるTLS変数
  • initial-exec: 実行バイナリ or 最初にロードが走る共有ライブラリ内の静的TLSブロック内に存在するTLS変数
  • local-dynamic: 動的ロードされるモジュールの中で、同一モジュール内での参照のみであるTLS変数
  • global-dynamic(デフォルト): 動的ロードされるものの中で、どこからでも参照可能なTLS変数

基本的にlocal-execのように局所的であるほど速く、global-dynamicが最も遅い。 とりわけxxx-dynamicは __tls_get_addr を経由する必要があるため有意に遅い。__tls_get_addr 遅いよね問題はIntelデベロッパーブログで ネタになるくらい遅い(解決策として何度もアクセスするような使い方をしている場合はローカル変数にキャッシュすることを提示している)。

(追記: オリジナル記事は追えなくなっているが、邦訳記事がある https://www.isus.jp/products/vtune/hidden-performance-cost/ )

jemallocは初期ではTLSアクセスモデルを規定しておらず、デフォルトのglobal-dynamicを用いていた。 しかしjemallocのように速度が求められる(もともとglibcよりよいmallocが欲しいという話であった)、またTLSを使ってロックの利用個所を減らす ようなアーキテクチャでは、より最適なTLSモデルを利用したいという要求がある。

また、mallocを提供するライブラリの場合はlibcが提供しているmallocと混在するような使い方はほぼないはず(と当時おそらく考えていたのだろう)、 であればより効率的なinitial-exec(jemallocは共有ライブラリとして提供する用途があるためlocal-execにはしていない)を使えばよいのではないか という考えは自然と出てくる。

そういうわけで、jemallocではコンパイラがサポートしている場合にはInitial exec TLS modelを有効にしてコンパイルするようになった。

しかし以下のIssueのように、Python界隈などでは共有リンクライブラリにリンクしている共有リンクライブラリをdlopen(3)で読み込むケースがあり、 この変更はそのようなケースで問題になっていた。

これは指摘されているように複数のmallocが混在することによるメモリリーク・データ破壊を引き起こす可能性があり、 そもそもLD_PRELOADを使って丸ごと差し替えるべきであるのだが、わりとそのように共有リンクライブラリをリンクしている 行儀の悪いPythonパッケージが多く、そこそこ影響範囲が多きな変更となっていた。

そこでjemallocはデフォルトではInitial exec TLS Modelを使うが、無効にするオプションを追加することにした。

RustでもPyO3経由でPythonのライブラリを使う場合に同様の問題にあたるため、このオプションを選択できるように追加された。

ちなみにmimallocでも同様の問題は起きており、やはりTLSモデルを選択可能とすることによって解決している。

参考資料

preExecFunctionのはなし

preExecFunctionとはなにか

preExecFunctionはspawnProcessでfork-execのあいだに行う任意の処理を記述するためにある。

import std.process;
import std.stdio;

void main()
{
    Config config = {
        preExecFunction: () nothrow @nogc @trusted {
            import core.sys.posix.unistd;
            if (setsid() == -1)
                return false;
            return true;
        },
    };
    auto pid = spawnProcess(["make", "all"],
                            std.stdio.stdin,
                            std.stdio.stdout,
                            std.stdio.stderr,
                            null,
                            config);
    scope(exit) pid.wait;
}

これまでD言語のspawnProcessは子プロセス起動する前にやりたい操作のよくあるケースとして、リダイレクト・パイプ・カレントディレクトリの移動などは方法を提供していたが、任意の操作を行う方法を提供してこなかった。そのためsetsidやsetuidのような処理を呼ぶことができなかった。

こういった操作を行う方法を提供する手段として、

  1. fork-exec間に実行されるような任意の操作を関数として差し込む (Python, Rustなど)
  2. fork-exec間に行いたい操作を制限しておき、キーワード引数を使って将来的な拡張に対応する (Ruby)

といった方法があげられる。2の方法は結局使いたいときにそのバージョンで使えないこともあるが、非同期シグナル安全な関数以外を誤って使ってしまうことを防げる。 PythonやRustでは1の方法を使っているが、ドキュメントに危険性を書いている。

参考資料