kubo39's blog

ただの雑記です。

D言語をプロダクションに使うには、ということを考える

D言語のコードを社のレポジトリにpushした。(まだmasterにmergeされていないので、先走り気味かもしれない)

D言語を使った理由は使いたかったというのもあるが、今回のケースでは

  • 使う箇所が部分的(言語はわりとなんでもいいところ)
  • 標準でjsonを扱うためのライブラリがついている(まあ今時の言語だとだいたいありそうな,Rustは標準でなかったので外れた)
  • バイナリを置けばいいだけのほうが楽そう(共有ライブラリのバージョンや置き場所の問題があるので、distroが同じという前提はあった)
  • 静的型チェックがある(これはチームの文化として)
  • それなりに安定して使える(NimとかCrystalとかPonyとかはこのへんで省かれる)

このへんを満たしているという点で、D言語を使うことが合理的な判断だったというところがある。この場合golangでよかった気がするけれど。

ユースケースを満たせばいいってものでもなく、D言語がチームで受け入れられるか、という点も大事になる。

チーム内での評価は

  • Pros
    • (C++がチームの標準言語なので)似たような構文なので読みやすい
    • 構文はわりとよい(型推論があるとか)
    • unittestを関数の近くに書けるところ
  • Cons
    • パターンマッチがない
    • 直和型がない(std.variantは、まあ…)

といった感じ。OCamlとかHaskellが好まれるチーム文化があるけれど、C++も書くチームなので受け入れられやすい。気がする。

D言語投入ありきで考えて、プロダクション投入するには、

  1. 小さく独立した部分ではじめる(動く状態を作ることが大事。時間かけるとけっきょくだれかにPythonとかで書かれてしまう)
  2. チームの文化に合致するか見定める(けっきょく他の人がレビューしないって言ったら別の言語で書くしかない)

この2点に集約される気がする。

Rustのconst_fnがそれほど使い勝手がよくなさそうだという話

現状ではnightlyでしか使えないが、Rustは部分的にコンパイル時に関数評価を定数畳み込みする const_fn という機能がある。

https://doc.rust-lang.org/beta/unstable-book/language-features/const-fn.html

そこでコンパイル時にバイト列を比較するコードを書いてみたが、これはコンパイルエラーになる。

#![feature(const_fn)]

const fn my_strcmp(s1: &[u8], s2: &[u8]) -> i32 {
    let len = if s1.len() <= s2.len() {
        s1.len()
    } else {
        s2.len()
    };

    for i in 0..len {
        if s1[i] != s2[i] {
            if s1[i] > s2[i] {
                return 1;
            } else {
                return -1;
            }
        }
    };

    if s1.len() < s2.len() {
        -1
    } else if s1.len() > s2.len() {
        1
    } else {
        0
    }
}

const X: i32 = my_strcmp(b"kubo39", b"kubo39");

fn main() {
    println!("{}", X);
    // println!("{}", my_strcmp(b"kubo39", b"kubo39")); // 実行時評価、これは動く
}

なるほど、const_fn は推移的でないとだめそうだ。

$ rustc --version
rustc 1.19.0-nightly (04145943a 2017-06-19)
$ rustc -O strcmp.rs
error[E0015]: calls in constant functions are limited to constant functions, struct and enum constructors
 --> strcmp.rs:4:18
  |
4 |     let len = if s1.len() <= s2.len() {
  |                  ^^^^^^^^

error[E0015]: calls in constant functions are limited to constant functions, struct and enum constructors
 --> strcmp.rs:4:30
  |
4 |     let len = if s1.len() <= s2.len() {
  |                              ^^^^^^^^

error[E0019]: constant function contains unimplemented expression type
 --> strcmp.rs:4:15
  |
4 |       let len = if s1.len() <= s2.len() {
  |  _______________^
5 | |         s1.len()
6 | |     } else {
7 | |         s2.len()
8 | |     };
  | |_____^

error: aborting due to previous error(s)

こういうコードはどうだろうか。

#![feature(const_fn)]

const fn lower_len(s1_len: usize, s2_len: usize) -> usize {
    if s1_len <= s2_len {
        s1_len
    } else {
        s2_len
    }
}

const fn my_strcmp(s1: &'static [u8], s1_len: usize, s2: &'static [u8], s2_len: usize) -> i32 {
    const len: usize = lower_len(s1_len, s2_len);

    for i in 0..len {
        if s1[i] != s2[i] {
            if s1[i] > s2[i] {
                return 1;
            } else {
                return -1;
            }
        }
    };

    if s1_len < s2_len {
        -1
    } else if s1_len > s2_len {
        1
    } else {
        0
    }
}

const S1: &'static [u8; 6] = b"kubo39";
const S1_LEN: usize = 6;
const S2: &'static [u8; 6] = b"kubo39";
const S2_LEN: usize = 6;

const X: i32 = my_strcmp(S1, S1_LEN, S2, S2_LEN);

fn main() {
    println!("{}", X);
    // println!("{}", my_strcmp(b"kubo39", b"kubo39"));
}

だめだ、 error[E0434]: can't capture dynamic environment in a fn item; use the || { ... } closure form instead のように怒られてしまっている。

$ rustc -O strcmp.rs
error[E0434]: can't capture dynamic environment in a fn item; use the || { ... } closure form instead
  --> strcmp.rs:12:34
   |
12 |     const len: usize = lower_len(s1_len, s2_len);
   |                                  ^^^^^^

error[E0434]: can't capture dynamic environment in a fn item; use the || { ... } closure form instead
  --> strcmp.rs:12:42
   |
12 |     const len: usize = lower_len(s1_len, s2_len);
   |                                          ^^^^^^

error: aborting due to previous error(s)

他にもいろいろ試したが、どうも制約が強すぎてそれほど利用できる箇所はなさそうなので現状あまり期待しないほうがよさそうだ。

Faster command line tool in Crystal?

こんな感じで巷ではやっているので、crystalはどうだろうと思い試してみた。

# coding: utf-8
# $ crystal build --release fastcmdline.cr
# crystal build --release fastcmdline.cr  7.94s user 0.09s system 101% cpu 7.937 total
# $ time ./fastcmdline googlebooks-eng-all-1gram-20120701-0 1 2
# max_key: 2006 sum: 22569013
# ./fastcmdline googlebooks-eng-all-1gram-20120701-0 1 2  8.68s user 0.69s system 125% cpu 7.465 total

if ARGV.size < 3
   puts "synopsis: #{__FILE__} filename keyfield valuefield"
   exit 1
 end

filename = ARGV[0]
key_field_index = ARGV[1].to_i
value_field_index = ARGV[2].to_i
max_field_index = [key_field_index, value_field_index].max
delim = "\t"

# ここで初期値いじってもあんま効果ない・・ 11, 1024 ~ 8192あたりでとったけど11が一番ましくらい
sum_by_key = Hash(String, Int64).new(initial_capacity: 11)

File.each_line(filename) do |line|
  # たぶんここでメモリ確保走る分遅い
  fields = line.split(delim).first(max_field_index + 1)
  if max_field_index < fields.size
    key = fields[key_field_index]
    field_value = fields[value_field_index].to_i64
    if sum_by_key[key]?.nil?
      sum_by_key[key] = field_value
    else
      sum_by_key[key] += field_value
    end
  end
end

if sum_by_key.empty?
  puts "No entries"
else
  max_key = ""
  max_value = 0.to_i64
  sum_by_key.each do |key, value|
    if value > max_value
      max_key = key
      max_value = value
    end
  end
  puts "max_key: #{max_key} sum: #{max_value}"
end

同じ環境でdmdやrustとやったところdmdが6.6secs、rustが1.1secsほどなのでcrystalは少し遅い。

アルゴリズム的なところ以外でいうと、

  1. Hashの初期メモリ確保が小さくて割当なおすので遅い
  2. splitが毎回メモリ確保するので遅い
  3. メモリ割り当て時にGCスキャンが走るので遅い

というあたりがありそうなかんじだった。

1.は初期値の11から1024,2048,4096,8192あたりに変えて試したけどとくに効果なし。 2.はちょっとめんどくさいのでやらなかったけど、ここは効果ありそう。 3.はGC.stopが生えていなかったのでパス。

といった形で、まあ結局大して最適化してない。それでもCPythonに比べるとだいぶ速いのですごいなあ。

Rustのlockとスコープのはなし

こういうコードを書くと当然deadlockする。

use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let mut lock = Arc::new(Mutex::new(false));
    let mut cloned_lock = lock.clone();

    let th = thread::spawn(move|| {
        loop {
            let mut quit = cloned_lock.lock().unwrap();
            if *quit {
                break;
            }
        }
    });

    let mut quit = lock.lock().unwrap();
    *quit = true;
    th.join();
}

ロックの開放タイミングをいいかんじにするとちゃんと終了する。

use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let mut lock = Arc::new(Mutex::new(false));
    let mut cloned_lock = lock.clone();

    let th = thread::spawn(move|| {
        loop {
            let mut quit = cloned_lock.lock().unwrap();
            if *quit {
                break;
            }
        }
    });

    {
        let mut quit = lock.lock().unwrap();
        *quit = true;
    }
    th.join();
}

さらに以下のように書いても正常に終了する。 おそらく変数スコープのところで獲得・開放してくれているのだろう。

use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let mut lock = Arc::new(Mutex::new(false));
    let mut cloned_lock = lock.clone();

    let th = thread::spawn(move|| {
        loop {
            let mut quit = cloned_lock.lock().unwrap();
            if *quit {
                break;
            }
        }
    });

    *lock.lock().unwrap() = true;
    th.join();
}

Rustいいなあ。

D言語のfizzbuzz

FizzBuzzD言語っぽいコードってどんな感じだろう、みたいな話があったので。

ふつうに実行時に書くならこんな感じだろうか。

import std.algorithm;
import std.conv : to;
import std.range;
import std.stdio;

void main()
{
    1.iota(16)
        .map!(a => (! (a % 15)) ? "fizzbuzz" : (! (a % 5)) ? "buzz" : (! (a % 3)) ? "fizz" : a.to!string )
        .each!writeln;
}

コンパイルfizzbuzzの愚直な例。 dmd -c fizzbuzz.d で動かせばコンパイル時出力確認できる。

string fizzbuzz(alias n)()
{
    import std.conv : to;
    static if (!(n % 15)) return "fizzbuzz";
    static if (!(n % 3)) return "fizz";
    static if (!(n % 5)) return "buzz";
    else return n.to!string;
}

pragma(msg, fizzbuzz!1);
pragma(msg, fizzbuzz!2);
pragma(msg, fizzbuzz!3);
pragma(msg, fizzbuzz!4);
pragma(msg, fizzbuzz!5);
pragma(msg, fizzbuzz!6);
pragma(msg, fizzbuzz!7);
pragma(msg, fizzbuzz!8);
pragma(msg, fizzbuzz!9);
pragma(msg, fizzbuzz!10);
pragma(msg, fizzbuzz!11);
pragma(msg, fizzbuzz!12);
pragma(msg, fizzbuzz!13);
pragma(msg, fizzbuzz!14);
pragma(msg, fizzbuzz!15);

再帰で書くのは逆順しか思いつかなかった。

auto fizzbuzz(alias n)()
{
    import std.conv : to;
    static if (n == 1) {
        return 1;
    } else {
        pragma(msg, (! (n % 15)) ? "fizzbuzz" : (! (n % 5)) ? "buzz" : (! (n % 3)) ? "fizz" : n.to!string );
        return fizzbuzz!(n - 1);
    }
}

pragma(msg, fizzbuzz!15);

Dconf 2017

1日目

Pointers Gone Wild: Memory Safety and D

Dの作者ことうぉるたん。コンパイラの恩恵を受けるために生ポインタを使うよりref使おうとか、return refやscope refとか追加した理由とか。スライドにD言語くんがいっぱい登場していた。

D as a Better C

基本CとC++のdisで、後半はプロジェクトの事例紹介。

あまり期待していたような話はなかった。

Where Art Thou, LDC?

LDCのコアコミッタの人。発表はみてなくて後でスライドみただけ。ここ最近のLDCの活動(PGOとかLTOとか最適化のためのオプション追加したとか)、将来のビジョン(リンカにLLD使いたいとか、RISC-Vターゲットとか)。

What’s GNU With You?

GDCの中の人の話。GDBD言語サポートがんばってる話(DWARF対応まわりとか、addr2line -C dlangとかでdemangleできるよ、とか)。

ABIまわりとか大変そうな雰囲気だった。ここは難しくてあまり理解できていない。

2日目

Things that Matter

C++界の巨人、Scott Mayer。効率/速度、ポータビリティ、ツーラビリティ(リファクタリングの話は笑った)、一貫性、インターフェース、コミットメントがこれまでの経験を通して大事であるという話。

Abstraction Cost and Optimization (LDC)

LDCでどういう風に書けば効率的なアセンブラを吐けるかという話。

いろいろなケースが紹介されている。単純な例でいうと、以下のコードはコンパイル時に定数になって効率的なアセンブラになっている。

import std.algorithm;
import std.range;

auto hoge()
{
    return 1.iota(10)
        .map!(a => a * 2)
        .reduce!((a, b) => a + b);
}

/*
$ ldc2 -c -O3 hoge.d
$ objdump -Cd -Mintel hoge.o
...
0000000000000000 <_D4hoge4hogeFNaNfZi>:
   0:   b8 5a 00 00 00          mov    eax,0x5a
   5:   c3                      retq
...
 */

また、以下のコードはポインタでなくrefを使うとループのところでSIMDを使ったアセンブラを生成してくれる。

extern(C):

alias T = int;
T byPtr(int n, T *c)
{
    T sum;
    for (int i=0; i < n; ++i)
        if (i > 0)
            sum += *c;
    return sum;
}

T byRef(int n, ref T c)
{
    T sum;
    for (int i=0; i < n; ++i)
        if (i > 0)
            sum += c;
    return sum;
}

int f(int);

生成されるアセンブリ (これもobjdump -Cd -Mintel)

ref.o:     file format elf64-x86-64


Disassembly of section .text.byPtr:

0000000000000000 <byPtr>:
   0:   31 c0                   xor    eax,eax
   2:   85 ff                   test   edi,edi
   4:   7e 56                   jle    5c <byPtr+0x5c>
   6:   8d 47 ff                lea    eax,[rdi-0x1]
   9:   89 f9                   mov    ecx,edi
   b:   83 e1 03                and    ecx,0x3
   e:   83 f8 03                cmp    eax,0x3
  11:   73 06                   jae    19 <byPtr+0x19>
  13:   31 d2                   xor    edx,edx
  15:   31 c0                   xor    eax,eax
  17:   eb 28                   jmp    41 <byPtr+0x41>
  19:   29 cf                   sub    edi,ecx
  1b:   31 d2                   xor    edx,edx
  1d:   31 c0                   xor    eax,eax
  1f:   90                      nop
  20:   85 d2                   test   edx,edx
  22:   7e 02                   jle    26 <byPtr+0x26>
  24:   03 06                   add    eax,DWORD PTR [rsi]
  26:   85 d2                   test   edx,edx
  28:   78 02                   js     2c <byPtr+0x2c>
  2a:   03 06                   add    eax,DWORD PTR [rsi]
  2c:   83 fa ff                cmp    edx,0xffffffff
  2f:   7c 02                   jl     33 <byPtr+0x33>
  31:   03 06                   add    eax,DWORD PTR [rsi]
  33:   83 fa fe                cmp    edx,0xfffffffe
  36:   7c 02                   jl     3a <byPtr+0x3a>
  38:   03 06                   add    eax,DWORD PTR [rsi]
  3a:   83 c2 04                add    edx,0x4
  3d:   39 d7                   cmp    edi,edx
  3f:   75 df                   jne    20 <byPtr+0x20>
  41:   85 c9                   test   ecx,ecx
  43:   74 17                   je     5c <byPtr+0x5c>
  45:   f7 d9                   neg    ecx
  47:   66 0f 1f 84 00 00 00    nop    WORD PTR [rax+rax*1+0x0]
  4e:   00 00 
  50:   85 d2                   test   edx,edx
  52:   7e 02                   jle    56 <byPtr+0x56>
  54:   03 06                   add    eax,DWORD PTR [rsi]
  56:   ff c2                   inc    edx
  58:   ff c1                   inc    ecx
  5a:   75 f4                   jne    50 <byPtr+0x50>
  5c:   c3                      ret    

Disassembly of section .text.byRef:

0000000000000000 <byRef>:
   0:   31 c0                   xor    eax,eax
   2:   85 ff                   test   edi,edi
   4:   0f 8e 58 01 00 00       jle    162 <byRef+0x162>
   a:   8b 0e                   mov    ecx,DWORD PTR [rsi]
   c:   31 f6                   xor    esi,esi
   e:   83 ff 08                cmp    edi,0x8
  11:   b8 00 00 00 00          mov    eax,0x0
  16:   0f 82 34 01 00 00       jb     150 <byRef+0x150>
  1c:   89 fa                   mov    edx,edi
  1e:   83 e2 f8                and    edx,0xfffffff8
  21:   b8 00 00 00 00          mov    eax,0x0
  26:   0f 84 24 01 00 00       je     150 <byRef+0x150>
  2c:   66 0f 6e c1             movd   xmm0,ecx
  30:   66 0f 70 c0 00          pshufd xmm0,xmm0,0x0
  35:   8d 42 f8                lea    eax,[rdx-0x8]
  38:   c1 e8 03                shr    eax,0x3
  3b:   44 8d 40 01             lea    r8d,[rax+0x1]
  3f:   41 83 e0 01             and    r8d,0x1
  43:   85 c0                   test   eax,eax
  45:   0f 84 a4 00 00 00       je     ef <byRef+0xef>
  4b:   41 8d 70 ff             lea    esi,[r8-0x1]
  4f:   29 c6                   sub    esi,eax
  51:   66 0f ef e4             pxor   xmm4,xmm4
  55:   66 0f 6f 2d 00 00 00    movdqa xmm5,XMMWORD PTR [rip+0x0]        # 5d <byRef+0x5d>
  5c:   00 
  5d:   66 44 0f 6f 05 00 00    movdqa xmm8,XMMWORD PTR [rip+0x0]        # 66 <byRef+0x66>
  64:   00 00 
  66:   66 44 0f 6f 0d 00 00    movdqa xmm9,XMMWORD PTR [rip+0x0]        # 6f <byRef+0x6f>
  6d:   00 00 
  6f:   66 44 0f 6f 15 00 00    movdqa xmm10,XMMWORD PTR [rip+0x0]        # 78 <byRef+0x78>
  76:   00 00 
  78:   66 44 0f 6f 1d 00 00    movdqa xmm11,XMMWORD PTR [rip+0x0]        # 81 <byRef+0x81>
  7f:   00 00 
  81:   66 0f ef f6             pxor   xmm6,xmm6
  85:   66 0f ef db             pxor   xmm3,xmm3
  89:   0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]
  90:   66 0f 6f cd             movdqa xmm1,xmm5
  94:   66 41 0f fe c8          paddd  xmm1,xmm8
  99:   66 0f 6f d5             movdqa xmm2,xmm5
  9d:   66 41 0f fe d1          paddd  xmm2,xmm9
  a2:   66 0f 6f fd             movdqa xmm7,xmm5
  a6:   66 0f 66 fc             pcmpgtd xmm7,xmm4
  aa:   66 0f 66 cc             pcmpgtd xmm1,xmm4
  ae:   66 0f db f8             pand   xmm7,xmm0
  b2:   66 0f db c8             pand   xmm1,xmm0
  b6:   66 0f fe fe             paddd  xmm7,xmm6
  ba:   66 0f fe cb             paddd  xmm1,xmm3
  be:   66 0f 6f dd             movdqa xmm3,xmm5
  c2:   66 41 0f fe da          paddd  xmm3,xmm10
  c7:   66 41 0f fe eb          paddd  xmm5,xmm11
  cc:   66 0f 66 d4             pcmpgtd xmm2,xmm4
  d0:   66 0f 66 dc             pcmpgtd xmm3,xmm4
  d4:   66 0f db d0             pand   xmm2,xmm0
  d8:   66 0f db d8             pand   xmm3,xmm0
  dc:   66 0f fe d7             paddd  xmm2,xmm7
  e0:   66 0f fe d9             paddd  xmm3,xmm1
  e4:   83 c6 02                add    esi,0x2
  e7:   66 0f 6f f2             movdqa xmm6,xmm2
  eb:   75 a3                   jne    90 <byRef+0x90>
  ed:   eb 10                   jmp    ff <byRef+0xff>
  ef:   66 0f ef d2             pxor   xmm2,xmm2
  f3:   66 0f 6f 2d 00 00 00    movdqa xmm5,XMMWORD PTR [rip+0x0]        # fb <byRef+0xfb>
  fa:   00 
  fb:   66 0f ef db             pxor   xmm3,xmm3
  ff:   45 85 c0                test   r8d,r8d
 102:   74 28                   je     12c <byRef+0x12c>
 104:   66 0f 6f 0d 00 00 00    movdqa xmm1,XMMWORD PTR [rip+0x0]        # 10c <byRef+0x10c>
 10b:   00 
 10c:   66 0f fe cd             paddd  xmm1,xmm5
 110:   66 0f ef e4             pxor   xmm4,xmm4
 114:   66 0f 66 cc             pcmpgtd xmm1,xmm4
 118:   66 0f db c8             pand   xmm1,xmm0
 11c:   66 0f fe d9             paddd  xmm3,xmm1
 120:   66 0f 66 ec             pcmpgtd xmm5,xmm4
 124:   66 0f db e8             pand   xmm5,xmm0
 128:   66 0f fe d5             paddd  xmm2,xmm5
 12c:   66 0f fe d3             paddd  xmm2,xmm3
 130:   66 0f 70 c2 4e          pshufd xmm0,xmm2,0x4e
 135:   66 0f fe c2             paddd  xmm0,xmm2
 139:   66 0f 70 c8 e5          pshufd xmm1,xmm0,0xe5
 13e:   66 0f fe c8             paddd  xmm1,xmm0
 142:   66 0f 7e c8             movd   eax,xmm1
 146:   39 fa                   cmp    edx,edi
 148:   89 d6                   mov    esi,edx
 14a:   74 16                   je     162 <byRef+0x162>
 14c:   0f 1f 40 00             nop    DWORD PTR [rax+0x0]
 150:   85 f6                   test   esi,esi
 152:   ba 00 00 00 00          mov    edx,0x0
 157:   0f 4f d1                cmovg  edx,ecx
 15a:   01 d0                   add    eax,edx
 15c:   ff c6                   inc    esi
 15e:   39 f7                   cmp    edi,esi
 160:   75 ee                   jne    150 <byRef+0x150>
 162:   c3                      ret    

あと最適化で消されたくないときは import ldc.attributes; して @weak を使うとかけっこう盛りだくさんでよかった。

まとめ?

システムプログラミング言語らしく比較的低レイヤの話が多かった印象。プロダクション事例とかはあまりない気がするが、自分のみるセクションが偏っているだけな気もする。

LDCの最適化とか

http://robert.ocallahan.org/2017/04/rust-optimizations-that-c-cant-do_5.html これを読んで、なるほどRustだとこういう最適化できるのね、というのを学んだ。

Here it’s really clear that the semantics of Rust are making this optimization possible. In C++ v could be a reference to a global variable which is modified by the callback function, in which case hoisting the load would be incorrect.

とあるようにC++だとvがグローバル変数への参照として渡ってきて、callbackの中でそのグローバル変数の書き換えが起こるかもしれない。 なんで、 [qword ptr [r15]] って感じでメモリをloadしにいく必要がある。(下にあるLink-Time Optimizationうんぬんは無視する)。 まあそのへんはsharedを型に使ってるD言語LDCあたりだとどうなるんだろう(まあなんとなくC++と同じ気はする、グローバル変数触らないという制約はもてない気する)と思ってざっとみてみる。

こういうコードを書く。

ulong foo(ref ulong v, void function() callback)
{
    auto sum = 0;
    foreach (_; 0 .. 100)
    {
        sum += v;
        callback();
    }
    return sum;
}

ldc2 -O -c -ofldc.o hoge.d でコンパイルした結果:

...
  20:   41 03 1e                add    (%r14),%ebx
  23:   41 ff d7                callq  *%r15
  26:   48 63 db                movslq %ebx,%rbx
  29:   ff cd                   dec    %ebp
  2b:   75 f3                   jne    20 <_D4hoge3fooFKmPFZvZm+0x20>

ループ内部はこんな感じになっていて、まあ期待(?)した結果になった。Intel記法だとdword ptrとかだしてくれるんでそっちしたほうがよかったかもしれない。

ただRust側で addとcallが並び変わってる理由がよくわかってない。

追記

pure を使えばいけるかも、と思ったが

$ ldc2 -O -c -ofldc.o hoge.d
hoge.d(7): Error: pure function 'hoge.foo' cannot call impure function pointer 'callback'

のように怒られてしまった。

ちょっと書き換えてみる。

ulong foo(ref ulong v, int function() pure nothrow @safe @nogc callback)
{
    auto sum = 0;
    foreach (_; 0 .. 100)
    {
        sum += v;
        callback();
    }
    return sum;
}

それでもだめなようだ

$ ldc2 -O -c -ofldc.o hoge.d
$ objdump -Mintel  -Cd ldc.o  # Intel記法にした
...
  20:   41 03 1e                add    ebx,DWORD PTR [r14]
  23:   41 ff d7                call   r15
  26:   48 63 db                movsxd rbx,ebx
  29:   ff cd                   dec    ebp
  2b:   75 f3                   jne    20 <_D4hoge3fooFKmPFNaNbNiNfZiZm+0x20>