憂鬱な午後のひととき

6月30日_

disp8*N

やっと意味とエンコード方法が分かってきた.

62F1ED485808     vaddpd zmm1, zmm2, [rax]
62F1ED48584801   vaddpd zmm1, zmm2, [rax+0x40]
62F1ED585808     vaddpd zmm1, zmm2, [rax]{1to8}
62F1ED58584801   vaddpd zmm1, zmm2, [rax+8]{1to8}

{1to2},{1to4},{1to8}の3種類だけ. ただしそれをするか否かの1bitの情報だけでレジスタサイズから1toNのNは決まる. インテルのマニュアルには{b}を使っているところもあった. {1toN}は冗長だからこちらのほうがいいな.

6月28日_

vcmppdサポート

vcmppd xmm1, xmm2, xmm3, 0
vcmppd ymm1, ymm2, ymm3, 0

ときたら

vcmppd zmm1, zmm2, zmm3, 0

だろうと思ったらzmmじゃなくてkだった.

vcmppd k4{k3}, zmm20, zmm30, {sae}, 5

に対応した.

6月27日_

メモリとsaeサポート

vaddpd(zmm1, zmm30, ptr [rax + rcx + 5]);
vaddpd(zmm2 | k1 | T_z, zmm2, zmm3 | T_rd_sae);

みたいなのが動くようになった. EVEXでは32bit displacement固定なのね. disp8*Nがどういうものかまだよく分かってない.

6月26日_

opmaskとzeroingサポート

vaddps zmm1{k1}{z}, zmm2, zmm3

をどう表現しようか悩んだが結局 | を使うことにした.

vaddps(zmm1 | k1 | T_z, zmm2, zmm3);

みたいな感じ. saeの方は素直にカンマを使おうとも思ったが, vcvtps2phみたいにその後ろに即値が来るパターンもあったので同様に | を使おうと思う.

vcvtps2ph ymm1, zmm2, 3
vcvtps2ph ymm1{k1}{z}, zmm2, {sae}, 3

もうちょっと何かよい文法は無かったのかと思う. XbyakもそうだがNASMも不正な文法に対するチェックは甘いようで

vcvtps2ph ymm1{k1}{z}{z}{z}{z}, zmm2, {sae},{sae},{sae},{sae}, 3

が通ってる. まあ実害は無いけどね.

6月25日_

体が動かん

最近体の疲れが尋常じゃない気がする.

6月24日_

ARM続き

一番簡単なcarryなし固定長加算を実装してみた(low_armv7.s).

ldm, stmで連続するメモリを複数レジスタに一度に入れられたり, !でポインタを更新したりとかなり高機能. とりあえず命令数が少ない方が速い感じ. メモリの測度にも影響しそうだけどそればっかりは分からん.

6月22日_

AVX-512 encoding

とりあえず"paddpd reg1, reg2, reg3"の形のもの. zmmレジスタは32個あるので5bit必要. それぞれの指数を

reg1Idx = [~R':~R:r1] ; R, R'は1bit, r1は3bit, ~は否定
reg2Idx = [~V':~vvvv] ; V'は1bit, vvvvは4bit
reg3Idx = [~X:~B:r2] ; X, Bは1bit, r2は3bit

と表す. 名前が分かりにくいけどマニュアルがそうなってるのでしかたない.

       p0                     p1                   p2
 R  X  B R' fix  mm     W  vvvv fix pp    z L'  L  b V'  aaa
 0| 0| 1| 1| 00| 01|  | 1| 1101| 1| 01| | 0| 1| 0| 0| 1| 000|

詰め込み具合が大変なことに. ちなみにregはxmm, ymmのときはL'Lはそれぞれ0, 1となる. zはzero拡張フラグ, aaaはkレジスタのindexが入るのかな.

6月21日_

ARM事始め

LLVMのARM用コード生成が全然当てにならないことが分かったので自分で書くことにした. SPARC, MIPS, TIのDSP, PowerPC, DAPDNA, CELLはやったことがあるがARMは初めてなのだった. というわけで今更ながら勉強を始める. マニュアルはRealView Compilation Tools アセンブラガイド バージョン 4.0

32bitの命令セットの名前がARM. Thumb, Thumb-2などの16bit命令セット拡張はあるけどとりあえず関係無さそう.

ARMは30本の32bit汎用レジスタ. ただし一度に見えるのはr0, ..., r12, sp(=r13), lr(=r14)らしい. spがスタックポインタ, lrはリンクレジスタで復帰アドレスが入る. APSRはアプリケーションプログラム状態レジスタ. x86のflagレジスタみたいなものか.

ALUステータスフラグに応じて変わる.

ALUステータスフラグ
    N ; 負になった
    Z ; zeroになった
    C ; carry発生
    V ; overflow
条件コード{cond}
    条件コードフラグが条件を満たしたときに実行される
EQ ; Zセット ; 等しい
NE ; Zクリア ; 等しくない
CS or HS ; Cセット ; 符号無し>=
CC or L0 ; Cクリア ; 符号無し<
MI ; Nセット ; < 0
PL ; Nクリア ; >= 0
VS ; Vセット ; overflow
VC ; Vクリア ; overflowなし
HI ; Cセット & Zクリア ; 符号なし>
LS ; Cクリア | Zセット ; 符号なし<=
GE ; N != V ;  符号付き>=
LT ; N != V ; 符号付き<
GT ; Zクリア  N = V ; 符号付き>
LE ; Zセット N != V ; 符号付き<=
AL ; すべて ; 無条件

一通りそろってる. 条件実行を使うとgcdのコード

int gcd(int a, int b)
{
  while (a != b) {
    if (a > b) {
      a = a - b;
    } else {
      b = b - a;
  }
  return a;
}

gcd ; a = r0, b = r1
  cmp r0, r1
  beq end
  blt less
  subs r0, r0, r1
  b gcd
less
  subs r1, r1, r0
  b gcd
end

とできると. なるほど. 手始めに

#include <stdint.h>

void add(uint64_t *z, const uint64_t *x, const uint64_t *y)
{
    z[0] = x[0] + y[0];
}

をgccとclangで

add1: // gcc
    ldrd    r2, [r2]
    stmfd   sp!, {r4, r5}
    ldrd    r4, [r1]
    adds    r2, r2, r4
    adc r3, r3, r5
    ldmfd   sp!, {r4, r5}
    strd    r2, [r0]
    bx  lr

    .align 2
add2: // clang
    ldm r1, {r3, r12}
    ldm r2, {r1, r2}
    adds    r1, r1, r3
    adc r2, r2, r12
    stm r0, {r1, r2}
    mov pc, lr

gccの方が速かった. stmfd, ldmfdはスタックにレジスタr4, r5を退避, 復元しているようだ. 引数は一つ目からr0, r1, r2. r3, r12は自由に使ってよさげ(本当か? 呼び出し規約が見当たらなかった).

bx lrとmov pc, lrの違いは何だろう. clangのmov pc, lrをbx lrにしたらgccのコードより速くなった. 特に問題なく動いているようだが.

6月19日_

疲れが溜まりすぎ

二日間, 体が全然動かなくてずっと寝ていた.

6月17日_

情報幾何学の基礎輪読会参加

計算お疲れさまでした. この本, 計算が結構迷走してるところがちらほらあって読みにくい. たとえばp.77の(3.21)の導出はこんな回りくどいことしなくてもp.60の(3.5)の直前の式でΓabc = 0とすればすぐでる.
p.78の導出もp.73とほとんど同じ.

ぼちぼち自分の番が回ってくるな. 準備しないと.

6月16日_

Xbyakのリファクタリング

AVX-512のEVEXエンコーディングに対応するため, Addressクラスまわりのリファクタリング. 32bit→64bit→AVXと複雑怪奇な拡張にともなって自分のコードもぐちゃぐちゃになっていた. 2007年から始めてるんだな. 思えば相当長い.

6月14日_

PBWT-sec with mcl

早稲田大学の清水さん、産総研の縫田さん、MSKCCのGunnarさんたちによるEfficient Privacy-Preserving String Search and an Application in Genomicsの実装. 私が今開発してるmclを使うpull reqを取り込んでもらった.

ドキュメントにある簡単なベンチマークで

従来版
23-mer match
00000100010000010000011
client(Wall): 68.9388
c2s, s2c(Mbyte): 1.8, 1.81
13.944u 0.184s 1:08.94 20.4%    0+0k 0+8072io 0pf+0w

最新版
23-mer match
00000100010000010000011
client(Wall): 14.7389
c2s, s2c(Mbyte): 1.84, 1.84
1.544u 0.296s 0:14.74 12.4%     0+0k 0+8072io 0pf+0w

となった.

6月13日_

早稲田

初めて行った. 会社からは東西線だと一本で行けるけど雨がきつかったので西早稲田から.

6月12日_

Opmaskレジスタ関連命令実装完了

次はいよいよEVEXまわりの実装. マニュアル読んでもなかなか頭にはいらない. Xbyakのコードもリファクタリングしないとかなり辛い.

6月11日_

『トーキョー・プリズン』(柳広司)

戦後の東京裁判にかかえられる囚人がいる刑務所での事件. ホームズに相当する人物が囚人でニュージーランド将校がワトソン役というのが斬新. 冒頭に感じた面白い話ではなく予想外の方向に進んだ.

6月10日_

VCでarm

そういえば最近のVCにはarmのクロスコンパイルツールも入ってるのだった. どんなものなんだろう. 32bit環境で試してみる.

#include <stdint.h>

void add(uint64_t *z, const uint64_t *x, const uint64_t *y)
{
    z[0] = x[0] + y[0];
}

  push        {r4,lr}
  ldrd        r4,r1,[r1]
  ldrd        r3,r2,[r2]
  adds        r3,r3,r4
  adcs        r2,r2,r1
  strd        r3,r2,[r0]
  pop         {r4,pc}

おお, ちゃんとでてる. これはどうやらthumbというモードのコードのようだ. clangの-march=thumbだと

    push    {r4, lr}
    ldr     r3, [r1]
    ldr     r1, [r1, #4]
    ldr     r4, [r2]
    ldr     r2, [r2, #4]
    adds    r3, r4, r3
    adcs    r2, r1
    str     r3, [r0]
    str     r2, [r0, #4]
    pop     {r4}
    pop     {r0}
    bx      r0

VCの方がよい感じに見えるなあ. ARM Intrinsicsを見るとadcに相当する関数は無さそうだ. 残念. ただそもそも_arm_smlalとか呼んでも無いって言われてしまう. 何かオプションが必要なのだろうか.

6月9日_

Opmaskレジスタ

XbyakのAVX-512対応を少しずつ始める. とりあえず簡単なkレジスタの対応から. マニュアルを見るとVEXエンコードも少し拡張されていた.

AVX2
VEX.[NDS/NDD/DS].[128,256,LZ,LIG].[66,F2,F3].0F/0F3A/0F38.[W0,W1,WIG] opcode [/r][ib,/is4]

AVX-512
VEX.[NDS/NDD/DS].[128,256,L0,L1,LIG].[66,F2,F3].0F/0F3A/0F38.[W0,W1,WIG] opcode [/r][ib,/is4]

L0やL1が増えている. kレジスタはビット長の情報は不要なのでそこを利用するようだ. kadd, kand, kandn, kor, kxnor, kxorのb, w, d, qサフィックス版がある.

たとえばkxnor k1, k2, k3. ぱっとみ, もはやIntel系CPUの命令とは思えない….

6月8日_

しんどい

体が動かず一回休み.

6月7日_

Re: 可変長マクロのコンマの扱いについてのトリック

##を使うとコンマが消えるのはトリックじゃなくてgcc/clangの拡張(Variadic Macros)と教えていただきました. ありがとうございます. 適当なこといってすいませんでした.

実際mcpp -- a portable C preprocessorで試すと

t.c:3: error: Not a valid preprocessing token ",1"
  macro "F" defined as: #define F(msg,...) fprintf(fp, msg,## __VA_ARGS__)    /* t.c:1 */
  from t.c: 3:    F("c=%d\n", 1);

というエラーになってました. 発言する前に確認しておくべきだった. 反省.

6月6日_

可変長マクロのコンマの扱いについてのトリック

可変長マクロで

#define F(msg, ...) fprintf(fp, msg, __VA_ARGS__)

F("c=%d\n", 1);
F("a=%d, b=%d\n", 2, 3);

という使い方をよくするけれどもF("xyz\n")とすると

fprintf(fp, "xyz\n", );

と最後に余計なコンマがついてエラーになってしまう. Comma omission and comma deletionによると, これを避けるためには##を使うとよいらしい.

#define G(msg, ...) fprintf(fp, msg, ## __VA_ARGS__)

G("c=%d\n", 1);
G("a=%d, b=%d\n", 2, 3);
G("xyz\n");

>clang t.c -E

fprintf(fp, "c=%d\n", 1);
fprintf(fp, "a=%d, b=%d\n", 2, 3);
fprintf(fp, "xyz\n");

本当だ. コンマが消えてる. 不思議だ.

ちなみにVisual StudioはもとのFでも気を利かせて余計なコンマを取り除いてくれる. ##を使っても問題ない.

6月5日_

『反コミュニケーション』(奥村隆)

さらっとしか読んでないけど面白かった. のっけから著者が私はコミュニケーションが嫌いだというのに面食らう. 本当にコミュニケーションをよくしたいのか, 本当に全部分かり合いたいのか, よいって何だという疑問.

各哲学者にインタビューしに行くという形式や最後の学生が書いた架空のレポートに対する討論も面白い.

1908年ジンメルの『社会学』によると
>「すべてのものはすべてのものとなんらかの相互作用の状態にある」
>存在するのは「個人」でも「社会」でもなく, 「相互作用」だけである.
だそうな. 場の量子論や素粒子物理学より前に人間に対してそういう考えをしていたんだ. へえ.

6月4日_

LLVM RAに対するオプション

@nothingcosmosさんがllc -O3 -pre-RA-sched=list-ilp -max-sched-reorder=128だと add1024が期待通りのコードを生成してくれる(arm以外)と教えてくださった. armで駄目なのは痛いけどありがとうございます.

RAはregister allocationの略. pre-RA-schedはCPUに応じたレジスタ割り当て最適化をする前の令のスケジューリングを設定するオプションのようだ. ILP(instruction level parallelism)とレジスタプレッシャーのバランス(ループアンロールしたらレジスタ足りないみたいなの)を変える.

6月3日_

_addcarry_u64の使い方

1日の_addcarry_u64はすばらしいが現状のVSで使うにはちょっと一工夫がいる. たとえば(64 * N)-bit加算関数を作ろうと

template<int N>
void add1(uint64_t *pz, const uint64_t *px, const uint64_t *py)
{
    unsigned char c = 0;
    for (int i = 0; i < N; i++) {
        c = _addcarry_u64(c, px[i], py[i], &pz[i]);
    }
}

とするとN = 2ですらよくないコードが生成される.

    xor     r9b, r9b
    mov     r10d, 2
    sub     r8, rdx
.lp:
    mov     rax, QWORD PTR [rdx]
    lea     rcx, QWORD PTR [rcx+8]
    add     r9b, -1
    adc     rax, QWORD PTR [r8+rdx]
    lea     rdx, QWORD PTR [rdx+8]
    mov     QWORD PTR [rcx-8], rax
    setb     r9b
    sub     r10, 1
    jne     .lp
    ret     0

おいおいループ展開されないのかよと3年近く前にも文句を言ったがVS2015でも改善されていない.

こういうのは今までも何度か書いたけど次のようなループ展開を強制するtemplateクラスを作れば回避できる.

template<class P, int begin, int end, int step = 1, int count = (step > 0 ? (end - begin + step - 1) / step : (begin - end - step - 1) / -step), int i = count>
struct Loop {
    Loop(P& p)
    {
        p.template calc<begin + (count - i) * step>();
        Loop<P, begin, end, step, count, i - 1> run(p);
    }
};

template<class P, int begin, int end, int step, int count>
struct Loop<P, begin, end, step, count, 0> {
    Loop(P&) { }
};

使い方はtemplate<int i>void cal()を持つクラスPを用意してcalcの中でやりたい処理を書く.

#include <intrin.h>
#include <stdint.h>

struct P {
    uint64_t *pz;
    const uint64_t *px;
    const uint64_t *py;
    unsigned char c;
    template<int i>
    void calc()
    {
        c = _addcarry_u64(c, px[i], py[i], &pz[i]);
    }
};

template<size_t N>
void add(uint64_t *pz, const uint64_t *px, const uint64_t *py)
{
    P p;
    p.pz = pz;
    p.px = px;
    p.py = py;
    p.c = 0;
    typedef Loop<P, 0, N> Lp;
    Lp run(p);
}

たとえばN = 8のときはこういうコードになる.

add:
    mov     rax, [rdx]
    add     rax, [r8]
    mov     [rcx], rax
    mov     rax, [rdx+8]
    adc     rax, [r8+8]
    mov     [rcx+8], rax
    mov     rax, [rdx+16]
    adc     rax, [r8+16]
    mov     [rcx+16], rax
    mov     rax, [rdx+24]
    adc     rax, [r8+24]
    mov     [rcx+24], rax
    mov     rax, [rdx+32]
    adc     rax, [r8+32]
    mov     [rcx+32], rax
    mov     rax, [rdx+40]
    adc     rax, [r8+40]
    mov     [rcx+40], rax
    mov     rax, [rdx+48]
    adc     rax, [r8+48]
    mov     [rcx+48], rax
    mov     rax, [rdx+56]
    adc     rax, [r8+56]
    mov     [rcx+56], rax
    ret

これがgccやclang, 他のCPUでもできればなあ.

6月2日_

LLVMの隠しオプション

1日のadd1024は実はxの値を2倍する場合はきれいな値を出力する.

define void @add1024(i1024* noalias %pz, i1024* noalias %px)
{
  %x = load i1024, i1024* %px
  %z = add i1024 %x, %x
  store i1024 %z, i1024* %pz
  ret void
}
// llc-3.8 -o - a.ll -O3 -march=x86 -x86-asm-syntax=intel
add1024:
    mov eax, dword ptr [esp + 4]
    mov ecx, dword ptr [esp + 8]
    mov edx, dword ptr [ecx]
    add edx, edx
    mov dword ptr [eax], edx
    mov edx, dword ptr [ecx + 4]
    adc edx, edx
    mov dword ptr [eax + 4], edx
    mov edx, dword ptr [ecx + 8]
    adc edx, edx
    ...

ただし引数が互いにオーバーラップしていないということを伝えるnoaliasを消すとデータを退避するコードになる.

z = x + yの場合はnoaliasに関わらずうまくいかない. x + xがうまくいくのだったらx + yをうまく処理してくれたってよいだろう. ただそういう最適化パスが入っていないわけではないと.

というわけでx + xのDAG(コードの木構造)がnoaliasありなしで違いが出るところを観察しようとした. SelectionDAG Instruction Selection Processを見ながら-view*なんとか系でDAGを出力させてみようとする.

  -view-dag-combine-lt-dags                 - Pop up a window to show dags before the post legalize types dag combine pass
  -view-dag-combine1-dags                   - Pop up a window to show dags before the first dag combine pass
  -view-dag-combine2-dags                   - Pop up a window to show dags before the second dag combine pass
  -view-edge-bundles                        - Pop up a window to show edge bundle graphs
  -view-isel-dags                           - Pop up a window to show isel dags as they are selected
  -view-legalize-dags                       - Pop up a window to show dags before legalize
  -view-legalize-types-dags                 - Pop up a window to show dags before legalize types
  -view-machine-block-freq-propagation-dags - Pop up a window to show a dag displaying how machine block frequencies propagate through the CFG.
    =none                                   -   do not display graphs.
    =fraction                               -   display a graph using the fractional block frequency representation.
    =integer                                -   display a graph using the raw integer fractional block frequency representation.
  -view-misched-cutoff=<uint>               - Hide nodes with more predecessor/successor than cutoff
  -view-misched-dags                        - Pop up a window to show MISched dags after they are processed
  -view-sched-dags                          - Pop up a window to show sched dags as they are processed
  -view-sunit-dags                          - Pop up a window to show SUnit dags after they are processed

が手元のclangは古くてHTMLに書かれたオプションが対応していない. 3.8でもだめ. HEADのclangを取ってきてコンパイルして試す. ついでにSelectionDAG.cppあたりのコードも眺めてみる. 試行錯誤して-view-misched-dagsの出力がz = x + xのnoaliasありなしが顕著に違った.

mischedはmachine instruction scheduling passというものらしい. -help-list-hiddenを使うとそれを制御するオプションがたくさんでてくる.

  -misched                    - Machine instruction scheduler to use
    =default                  -   Use the target's default scheduler choice.
    =converge                 -   Standard converging scheduler.
    =ilpmax                   -   Schedule bottom-up for max ILP
    =ilpmin                   -   Schedule bottom-up for min ILP
    =shuffle                  -   Shuffle machine instructions alternating directions
  -misched-bottomup           - Force bottom-up list scheduling
  -misched-cluster            - Enable memop clustering.
  -misched-cutoff=<uint>      - Stop scheduling after N instructions
  -misched-cyclicpath         - Enable cyclic critical path analysis.
  -misched-dcpl               - Print critical path length to stdout
  -misched-fusion             - Enable scheduling for macro fusion.
  -misched-limit=<uint>       - Limit ready list to N instructions
  -misched-only-block=<uint>  - Only schedule this MBB#
  -misched-only-func=<string> - Only schedule this function
  -misched-postra             - Run MachineScheduler post regalloc (independent of preRA sched)
  -misched-regpressure        - Enable register pressure scheduling.
  -misched-topdown            - Force top-down list scheduling

まだ中身を理解してないけどいろいろ設定していたら-misched-topdownや-misched=ilpmaxなどで欲しいコードになることが分かった.

    mov eax, dword ptr [esp + 16]
    mov ecx, dword ptr [esp + 20]
    mov edx, dword ptr [esp + 24]
    mov edi, dword ptr [ecx + 4]
    mov ebx, dword ptr [ecx + 8]
    mov esi, dword ptr [ecx]
    add esi, dword ptr [edx]
    adc edi, dword ptr [edx + 4]
    adc ebx, dword ptr [edx + 8]
    mov dword ptr [eax + 8], ebx
    mov dword ptr [eax + 4], edi
    mov dword ptr [eax], esi
    mov esi, dword ptr [edx + 12]
    adc esi, dword ptr [ecx + 12]
    mov dword ptr [eax + 12], esi

ただこれらの設定はいつもしてよいというわけではないようだ. 他の関数では逆にspillが増えることもあった(まだ詳細は見ていない).

あと肝心なarmでspillしっぱなしは変わらなかった. orz.

仕様が変わるといえばloadやgetelementptrなどの基本命令も3.6と3.8で違っていて焦った.

%x = load i64* %px      ; 3.6で動くが3.8でエラー
%x = load i64, i64* %px ; 3.8で動くが3.6でエラー

さして意味のある差異にも見えないのに後方互換性も前方互換性もなく変えないでほしい.

あと_addcarry_u64がclangでエラーになるのはFixing PR26558: the adc builtins do not require the adx target attributeで直ってると@trairiaさんに教えていただいた. ありがとうございます.

6月1日_

コンパイラがサポートする多倍長演算用intrinsic関数の現状

mclを32bitモードでコンパイルすると非常に遅かった理由はLLVMのaddのせいだった. 具体的には次のコードがひどいコードになってしまうせい.

define void @add1024(i1024* noalias %pz, i1024* noalias %px, i1024* noalias %py)
{
; version 3.8
  %x = load i1024, i1024* %px
  %y = load i1024, i1024* %py

; version 3.6
;  %x = load i1024* %px
;  %y = load i1024* %py

  %z = add i1024 %x, %y
  store i1024 %z, i1024* %pz
  ret void
}

llcの出力を見ると

add1024:
    push    rbp
    push    r15
    push    r14
    push    r13
    push    r12
    push    rbx
    mov rax, qword ptr [rsi + 120]
    mov qword ptr [rsp - 8], rax # 8-byte Spill
    mov r13, qword ptr [rsi + 112]
    mov rax, qword ptr [rsi + 104]
    mov qword ptr [rsp - 16], rax # 8-byte Spill
    mov r12, qword ptr [rsi + 96]
    mov rax, qword ptr [rsi + 88]
    mov qword ptr [rsp - 24], rax # 8-byte Spill
    mov rax, qword ptr [rsi + 80]
    mov qword ptr [rsp - 32], rax # 8-byte Spill
    mov r8, qword ptr [rsi + 72]
    mov rcx, qword ptr [rsi + 64]
    mov rax, qword ptr [rsi + 56]
    mov r14, qword ptr [rdx + 16]
    mov r15, qword ptr [rdx]
    mov rbp, qword ptr [rdx + 8]
    add r15, qword ptr [rsi]
    adc rbp, qword ptr [rsi + 8]
    adc r14, qword ptr [rsi + 16]
    mov rbx, qword ptr [rsi + 48]
    ...

xとyの加算は下位bitからするのだからそれをそのままpzに書き込んでくれればいいのに1024bit分の加算が終わってからでないとpzに入れてくれない. そのためレジスタ溢れが起こりstackに退避してloadし直している.

本当なら

  mov rax, [rsi]
  add rax, [rdi]
  mov [rdx], rax
  mov rax, [rsi + 8]
  adc rax, [rdi + 8]
  mov [rdx + 8], rax
  mov rax, [rsi + 16]
  adc rax, [rdi + 16]
  mov [rdx + 16], rax
  ...

というコードになって欲しいのだが.

64bitで256bit加算のときはwork areaがregisterに取れていたので顕在しなかった. 32bit環境で1024bit加算をするとえらいことになる. 困った. 何かうまい方法ないでしょうか.

で, LLVMに頼らずコンパイラに任せられないかと調べ直してみた. 最近のコンパイラはcarry flagを扱うintrinsic関数を持っている.

現状一番優秀なのはVC. 2年以上前から_addcarry_u64が入っている. この関数が理想系. 出力も完璧.

#include <stdint.h>
#include <intrin.h>
void add128(uint64_t *pz, const uint64_t *px, const uint64_t *py)
{
    unsigned char c = 0;
    c = _addcarry_u64(c, px[0], py[0], (unsigned long long*)&pz[0]);
    _addcarry_u64(c, px[1], py[1], (unsigned long long*)&pz[1]);
}
//
add128:
  mov rax, [rdx]
  add rax, [r8]
  mov [rcx], rax
  mov rax, [rdx+8]
  adc rax, [r8+8]
  mov [rcx+8], rax
  ret

ただclang-3.8でコンパイルするとコンパイラがエラーを吐く(何故?). gcc-6.1ではしょぼいコードになる. あと多分ARM用だとこの関数は無い.

// gcc-6.1 -S -Ofast
add128:
    movq    (%rsi), %rax
    addq    (%rdx), %rax
    setc    %cl
    movq    %rax, (%rdi)
    movq    8(%rdx), %rax
    addb    $-1, %cl
    adcq    8(%rsi), %rax
    movq    %rax, 8(%rdi)
    ret

しばらく前から導入された__builtin_uaddl_overflowはあくまでoverflowを検出するためのものでcarryの入力がないので使えない.

clangにはmultiprecision arithmetic builtinsというのがあるようだ.

#include <stdint.h>
void add192(uint64_t *pz, const uint64_t *px, const uint64_t *py)
{
    unsigned long in = 0;
    pz[0] = __builtin_addcl(px[0], py[0], in, &in);
    pz[1] = __builtin_addcl(px[1], py[1], in, &in);
    pz[2] = __builtin_addcl(px[2], py[2], in, &in);
}

残念ながらこちらもclang-3.8で

add192:
    mov rax, qword ptr [rsi]
    add rax, qword ptr [rdx]
    sbb rcx, rcx
    and ecx, 1
    mov qword ptr [rdi], rax
    mov rax, qword ptr [rsi + 8]
    add rax, qword ptr [rdx + 8]
    sbb r8b, r8b
    add rax, rcx
    sbb cl, cl
    or  cl, r8b
    and cl, 1
    movzx   ecx, cl
    mov qword ptr [rdi + 8], rax
    mov rax, qword ptr [rdx + 16]
    add rax, qword ptr [rsi + 16]
    add rax, rcx
    mov qword ptr [rdi + 16], rax
    ret

という, わざわざ独自intrinsicを用意しといておまえは一体何がしたいんだ, というコードを生成する. そもそもcarryにin, outがあるのが筋が悪い.

結局, 現状で多倍長計算のためのintrinsicでまともに使えるのはVCの_addcarry_u64だけというのは2年前から変わっていなかった. やっぱり手書きアセンブラになってしまうのか. どうしよう.


御意見は光成滋生<herumi@nifty.com>までお願いします