kubo39's blog

ただの雑記です。

STM32F3DISCOVERYをD言語から動かすライブラリを書いている

https://github.com/kubo39/stm32f3discovery で開発中です。LDC前提。とりあえずのLチカと例外のハンドリングとかくらいしかない。

Lチカのプログラムはこんな感じの抽象度で書けます。

import stm32f3discovery;

import led;

extern(C):
@nogc:
nothrow:

void main()
{
    pragma(LDC_never_inline);

    initLED();

    auto ticks = 100000;

    while (true)
    {
        foreach (led; LEDS)
        {
            led.on;
            delay(ticks);
            led.off();
            delay(ticks);
        }
    }
}

void delay(uint n)
{
    pragma(LDC_never_inline);

    foreach (_; 0 .. n)
    {
        // nop
    }
}

作り途中なので課題はまだまだありますが、とりあえず見えてる範囲だと

  • 実装量が全然足りてない(タイマーとかusartとか追加していく)
  • モジュール構成がいけてない
  • 内部の抽象化がまだまだきれいになってない(生ポインタでやってたり)
  • パッケージングどうするか(dubが貧弱に感じてるのでMakefileにしてるけど別によかったのかもしれない)
  • ARM Cortex-Mで共通のものはさらにライブラリとしてくくりだしたい

とかです。

こういうことをやってるとグローバルな変数参照しようとしてlibcの __tls_get_addr を参照しようとしてリンクエラーになったり、extern(D) void mainにしてて_Dmainとして扱われて死亡したりD言語特有の現象に遭遇して詳しくなれたりするようなしないような気がします。

他にあとD言語のいいところ、、

  • C言語との連携
  • @nogcやnothrowのチェックがある
  • Cよりずっとstrictな型検査
  • typeofとか使うと便利

今のところこのへんですかね。

STM32F3DISCOVERYでD言語のプログラムを実行する

LDCコンパイラでSTM32F3DISCOVERYで動く実行バイナリをいろいろ苦労して作成した。

いろいろ罠

LDCのオプションは罠だらけ

  • march,mcpuの指定が罠

    • float ABIの指定でhardが効かない、soft ABIとなる
  • tripleの指定が罠

    • eabihfだとsoft-float ABIになってしまう、gnueabihfにしないとhard-float ABIにならない
    • OSにunknownが指定できない、とりあえずLinuxを指定して対処

リンカまわり

  • エントリポイントが0x1バイトずれる
    • 外から-eオプションで渡して回避

最終的な実行バイナリ生成コマンド

$ ldc2 -mtriple=thumbv7em-none-linux-gnueabihf -c -g main.d
$ arm-none-eabi-ld main.o -e 0x8000009 -T layout.ld -nostartfiles -o main.bin

readelfの出力

  • エントリポイントアドレスが 0x8000009 になっていればよい
  • Float ABIが hard-float ABI になっていればよい
$ arm-none-eabi-readelf -h main.bin
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x8000009
  Start of program headers:          52 (bytes into file)
  Start of section headers:          1548 (bytes into file)
  Flags:                             0x5000400, Version5 EABI, hard-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         16
  Section header string table index: 13

objdumpの出力

このような内容になった

$ arm-none-eabi-objdump -Cd main.bin

main.bin:     file format elf32-littlearm


Disassembly of section .text:

08000000 <_reset-0x8>:
 8000000:       2000a000        .word   0x2000a000
 8000004:       08000009        .word   0x08000009

Disassembly of section _reset:

08000008 <_reset>:
 8000008:       b082            sub     sp, #8
 800000a:       202a            movs    r0, #42 ; 0x2a
 800000c:       9000            str     r0, [sp, #0]
 800000e:       9001            str     r0, [sp, #4]
 8000010:       e7ff            b.n     8000012 <_reset+0xa>
 8000012:       2000            movs    r0, #0
 8000014:       2800            cmp     r0, #0
 8000016:       d102            bne.n   800001e <_reset+0x16>
 8000018:       e7ff            b.n     800001a <_reset+0x12>
 800001a:       e7ff            b.n     800001c <_reset+0x14>
 800001c:       e7f9            b.n     8000012 <_reset+0xa>
 800001e:       b002            add     sp, #8
 8000020:       4770            bx      lr

OpenOCD

APTで入れたものであればこのような表示になる。

$ openocd -f board/stm32f3discovery.cfg
Open On-Chip Debugger 0.9.0 (2015-09-02-10:42)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results mi
ght differ compared to plain JTAG/SWD
none separate
srst_only separate srst_nogate srst_open_drain connect_deassert_srst
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v16 API v2 SWIM v0 VID 0x0483 PID 0x3748
Info : using stlink api v2
Info : Target voltage: 2.917647
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints

GDB

GDBでリモート接続する。

$ arm-none-eabi-gdb -q main.bin
Reading symbols from main.bin...done.
(gdb) target remote :3333
Remote debugging using :3333

loadしてSTM32F3DISCOVERYボード上で実行できるようにする。

(gdb) load
Loading section .text, size 0x8 lma 0x8000000
Loading section _reset, size 0x1a lma 0x8000008
Start address 0x8000008, load size 34
Transfer rate: 244 bytes/sec, 17 bytes/write.

エントリポイントは 0x08000009 となっているが、プログラムの先頭は 0x08000008 となっている。

(gdb) disas
Dump of assembler code for function main:
=> 0x08000008 <+0>:     sub     sp, #8
   0x0800000a <+2>:     movs    r0, #42 ; 0x2a
   0x0800000c <+4>:     str     r0, [sp, #0]
   0x0800000e <+6>:     str     r0, [sp, #4]
   0x08000010 <+8>:     b.n     0x8000012 <main+10>
   0x08000012 <+10>:    movs    r0, #0
   0x08000014 <+12>:    cmp     r0, #0
   0x08000016 <+14>:    bne.n   0x800001e <main+22>
   0x08000018 <+16>:    b.n     0x800001a <main+18>
   0x0800001a <+18>:    b.n     0x800001c <main+20>
   0x0800001c <+20>:    b.n     0x8000012 <main+10>
   0x0800001e <+22>:    add     sp, #8
   0x08000020 <+24>:    bx      lr
End of assembler dump.

ステップ実行で確認。たしかに動いているようだ。

(gdb) step
main () at main.d:13
13          auto x = 42;
(gdb) print x
$1 = 42
(gdb) step
14          y = x;
(gdb) print y
$2 = 42
(gdb) print &x
$3 = (int *) 0x20009ff8
(gdb) print &y
$4 = (int *) 0x20009ffc

LDCでSTM32F103のバイナリを作ろうとしてできなかった

Cortex-M3な環境を試してみる。

コードを以下のようなかんじで用意する。

import ldc.attributes;

extern(C):
@nogc:
nothrow:

pragma(LDC_no_moduleinfo);

@(section("_reset"))
void _reset()
{
    int y = void;
    auto x = 42;
    y = x;

    while (true) {}
}
ENTRY(_reset);

MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 128K
  RAM : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
  .text :
  {
    /* Vector table */
    LONG(ORIGIN(RAM) + LENGTH(RAM))
    LONG(_reset + 1);

    /* Reset handler */
    _reset = .;
    *(.text._reset)

    *(.text*)
  } > FLASH

  /DISCARD/ :
  {
    *(.ARM.exidx*)
    *(.note.gnu.build-id*)
  }
}

実行バイナリを作る。

$ ldc2 -march=thumb -mcpu=cortex-m3 -c -g main.d
$ arm-none-eabi-ld main.o -T layout.ld -nostartfiles -o main.bin

OpenOCDを起動。

$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f1x.cfg

gdbからプログラムを流しこもうとするとコネクションが閉じられる。

$ gdb -q main.bin
Reading symbols from main.bin...done.
(gdb) target remote :3333
Remote debugging using :3333
warning: Architecture rejected target-supplied description
0xffbefdde in ?? ()
(gdb) load
Loading section .text, size 0x8 lma 0x8000000
Loading section _reset, size 0x1a lma 0x8000008
Start address 0x8000008, load size 34
Remote connection closed

OpenOCD側の出力はこのような感じ。

他のボードと同じように Error: gdb requested a non-existing register が出力されている。

Info : accepting 'gdb' connection on tcp/3333
target state: halted
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000008 msp: 0x20005000
target state: halted
target halted due to breakpoint, current mode: Thread 
xPSR: 0x61000000 pc: 0x2000003a msp: 0x20005000
target state: halted
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000008 msp: 0x20005000
Error: gdb requested a non-existing register
Info : dropped 'gdb' connection

objdumpで実行バイナリをみる。があまりよくわからない。

$ arm-none-eabi-objdump -Cd main.bin

main.bin:     file format elf32-littlearm


Disassembly of section .text:

08000000 <_reset-0x8>:
 8000000:       20005000        .word   0x20005000
 8000004:       08000009        .word   0x08000009

Disassembly of section _reset:

08000008 <_reset>:
 8000008:       b082            sub     sp, #8
 800000a:       202a            movs    r0, #42 ; 0x2a
 800000c:       9000            str     r0, [sp, #0]
 800000e:       9001            str     r0, [sp, #4]
 8000010:       e7ff            b.n     8000012 <_reset+0xa>
 8000012:       2000            movs    r0, #0
 8000014:       2800            cmp     r0, #0
 8000016:       d102            bne.n   800001e <_reset+0x16>
 8000018:       e7ff            b.n     800001a <_reset+0x12>
 800001a:       e7ff            b.n     800001c <_reset+0x14>
 800001c:       e7f9            b.n     8000012 <_reset+0xa>
 800001e:       b002            add     sp, #8
 8000020:       4770            bx      lr

Rustで生成したものとreadelfを比較してみると、エントリポイントが0x1ずれているのが問題であることに気づいた。

以前書いた記事 で書いたfloat ABIの違いは関係ないようだ。さらに調べるとLLVMに渡すtripleのオプションでhardware floatを渡すことができた。ただしOSの関してはunknownを渡すことはできないようだ。

エントリポイントを外からいじってみる。

$ ldc2 -mtriple=thumbv7m-none-linux-eabi -c -g main.d
$ arm-none-eabi-ld main.o -e 0x8000009 -T layout.ld -nostartfiles -o main.bin
$ arm-none-eabi-objdump -Cd main.bin

main.bin:     file format elf32-littlearm


Disassembly of section .text:

08000000 <_reset-0x8>:
 8000000:       20005000        .word   0x20005000
 8000004:       08000009        .word   0x08000009

Disassembly of section _reset:

08000008 <_reset>:
 8000008:       b082            sub     sp, #8
 800000a:       202a            movs    r0, #42 ; 0x2a
 800000c:       9000            str     r0, [sp, #0]
 800000e:       9001            str     r0, [sp, #4]
 8000010:       e7ff            b.n     8000012 <_reset+0xa>
 8000012:       2000            movs    r0, #0
 8000014:       2800            cmp     r0, #0
 8000016:       d102            bne.n   800001e <_reset+0x16>
 8000018:       e7ff            b.n     800001a <_reset+0x12>
 800001a:       e7ff            b.n     800001c <_reset+0x14>
 800001c:       e7f9            b.n     8000012 <_reset+0xa>
 800001e:       b002            add     sp, #8
 8000020:       4770            bx      lr

エントリポイントが0x8000009になっていることを確認。

$ arm-none-eabi-readelf -h main.bin
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x8000009
  Start of program headers:          52 (bytes into file)
  Start of section headers:          1548 (bytes into file)
  Flags:                             0x5000200, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         16
  Section header string table index: 13

しかし結果は変わらず、うまくいかなかった。

QEMUを使ってCortex M3なバイナリを動かしてみる

環境

$ cat /proc/version
Linux version 4.8.0-41-generic (buildd@lgw01-18) (gcc version 6.2.0 20161005 (Ubuntu 6.2.0-5ubuntu12) ) #44-Ubuntu SMP Fri Mar 3 15:27:17 UTC 2017
$ ldc2 -version| head -1
LDC - the LLVM D compiler (1.0.0):

インストール方法

$ sudo apt install -y gcc-arm-none-eabi gdb-arm-none-eabi qemu-system-arm ldc

手順

まず以下のようにD言語のプログラムを書いてみる。

import ldc.attributes;

extern(C):
@nogc:
nothrow:

@(section("_reset"))
void main()
{
    int y = void;
    auto x = 42;
    y = x;

    while (true) {}
}
$ ldc2 -march=thumb -mcpu=cortex-m3 -c -g main.d
$ arm-none-eabi-ld main.o -T layout.ld -nostartfiles -o main.bin

このままでは以下のようなエラーが出てしまう。

main.o: In function `ldc.register_dso':
main.d:(.text.ldc.register_dso+0x60): undefined reference to `_d_dso_registry'

_d_dso_registry はDランタイム内部で利用される関数である。

http://arsdnet.net/this-week-in-d/2016-oct-09.html

今回生成するバイナリでは必要ないものであるので、 LDC_no_moduleinfo を使って無効にする。

LDC-specific_language_changes

このようになっているはず。

import ldc.attributes;

extern(C):
@nogc:
nothrow:

pragma(LDC_no_moduleinfo);

@(section("_reset"))
void main()
{
    int y = void;
    auto x = 42;
    y = x;

    while (true) {}
}

QEMUを起動してgdbサーバをたてておく。

$ qemu-system-arm \
    -cpu cortex-m3 \
    -machine lm3s6965evb \
    -gdb tcp::3333 \
    -S \
    -nographic -monitor null \
    -serial null \
    -kernel main.bin

gdbを起動してstep実行。ループに入ったところで制御がきかなくなってしまった。

$ arm-none-eabi-gdb -q main.bin
Reading symbols from main.bin...done.
(gdb) target remote :3333
Remote debugging using :3333
main () at main.d:10
10      void main()
(gdb) step
13          auto x = 42;
(gdb) step
14          y = x;
(gdb) print x
$1 = 42
(gdb) print &x
$2 = (int *) 0x2000fff8
(gdb) print y
$3 = 0
(gdb) print &y
$4 = (int *) 0x2000fffc
(gdb)

STM32F3DISCOVERYボード向けに実行プログラムをLDCで作ってみようとしてハマった

表題のとおり、STM32F3DISCOVERYボード向けに実行プログラムをLDCで作ってみようとしてハマった。

D言語ソースコード

import ldc.attributes;

extern(C):
@nogc:
nothrow:

pragma(LDC_no_moduleinfo);

@(section("_reset"))
void _reset()
{
    int y = void;
    auto x = 42;
    y = x;

    while (true) {}
}

リンカスクリプト

ENTRY(_reset);

MEMORY
{
  CCRAM : ORIGIN = 0x10000000, LENGTH = 8K
  FLASH : ORIGIN = 0x08000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 40K
}

SECTIONS
{
  .text :
  {
    /* Vector table */
    LONG(ORIGIN(RAM) + LENGTH(RAM))
    LONG(_reset + 1);

    /* Reset handler */
    _reset = .;
    *(.text._reset)

    *(.text*)
  } > FLASH

  /DISCARD/ :
  {
    *(.ARM.exidx*)
    *(.note.gnu.build-id*)
  }
}

実行バイナリを作る手順はこのような感じ。

$ ldc2 -march=thumb -mcpu=cortex-m4 -c -g main.d
$ arm-none-eabi-ld main.o -T layout.ld -nostartfiles -o main.bin

OpenOCDを起動する。

$ openocd -f board/stm32f3discovery.cfg
Open On-Chip Debugger 0.9.0 (2015-09-02-10:42)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results m$
ght differ compared to plain JTAG/SWD
none separate
srst_only separate srst_nogate srst_open_drain connect_deassert_srst
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v16 API v2 SWIM v0 VID 0x0483 PID 0x3748
Info : using stlink api v2
Info : Target voltage: 2.919073
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints

別ターミナルからGDBを使ってプログラムを実行しようとしたところ、以下のような状況になった。

$ gdb -q main.bin
Reading symbols from main.bin...done.
(gdb) target remote :3333
Remote debugging using :3333
warning: Architecture rejected target-supplied description
0x00000000 in ?? ()
(gdb) load
Loading section .text, size 0x8 lma 0x8000000
Loading section _reset, size 0x1a lma 0x8000008
Start address 0x8000008, load size 34
Remote connection closed

OpenOCD側の出力はこのようになった。

Info : accepting 'gdb' connection on tcp/3333
undefined debug reason 7 - target needs reset

adapter speed: 1000 kHz
target state: halted
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000008 msp: 0x2000a000
adapter speed: 8000 kHz
target state: halted
target halted due to breakpoint, current mode: Thread 
xPSR: 0x61000000 pc: 0x2000003a msp: 0x2000a000
adapter speed: 1000 kHz
target state: halted
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000008 msp: 0x2000a000
Error: gdb requested a non-existing register
Info : dropped 'gdb' connection

gdb側でloadしようとするとconnection closedになってしまう。

$ arm-none-eabi-objdump -Cd main.bin

main.bin:     file format elf32-littlearm


Disassembly of section .text:

08000000 <_reset-0x8>:
 8000000:       2000a000        .word   0x2000a000
 8000004:       08000009        .word   0x08000009

Disassembly of section _reset:

08000008 <_reset>:
 8000008:       b082            sub     sp, #8
 800000a:       202a            movs    r0, #42 ; 0x2a
 800000c:       9000            str     r0, [sp, #0]
 800000e:       9001            str     r0, [sp, #4]
 8000010:       e7ff            b.n     8000012 <_reset+0xa>
 8000012:       2000            movs    r0, #0
 8000014:       2800            cmp     r0, #0
 8000016:       d102            bne.n   800001e <_reset+0x16>
 8000018:       e7ff            b.n     800001a <_reset+0x12>
 800001a:       e7ff            b.n     800001c <_reset+0x14>
 800001c:       e7f9            b.n     8000012 <_reset+0xa>
 800001e:       b002            add     sp, #8
 8000020:       4770            bx      lr

readlelfでみるとsoft-float ABIになっている。

STM32F3DISCOVERYはCortex-M4Fなのでhardware floatにしなければないらないようだ。

$ arm-none-eabi-readelf -h main.bin
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x8000008
  Start of program headers:          52 (bytes into file)
  Start of section headers:          1568 (bytes into file)
  Flags:                             0x5000200, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         16
  Section header string table index: 13

float-abiを指定してみる。

$ ldc2 -march=thumb -mcpu=cortex-m4 -c -g -float-abi=hard main.d
$ arm-none-eabi-ld main.o -T layout.ld -nostartfiles -o main.bin

結果変わらず。

$ arm-none-eabi-readelf -h main.bin
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x8000008
  Start of program headers:          52 (bytes into file)
  Start of section headers:          1568 (bytes into file)
  Flags:                             0x5000200, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         16
  Section header string table index: 13

LDCはtarget tripleの指定もできる。これが通って欲しい。けどだめそうだ。 軽く本体のソースを読んだ感じ、LDC側はthumbv7emやeabihfを解釈できないようだった。

$ ldc2 -mtriple=thumbv7em-unknown-none-eabihf -c -g main.d
Targeting 'thumbv7em-unknown-none-eabihf' (CPU '' with features '')
Error: target 'thumbv7em-unknown-none-eabihf' is not yet supported

$ ldc2 -march=thumb -mcpu=cortex-m4 -float-abi=hard -mabi=eabihf -c -g main.d でもsoft-float ABIなバイナリが生成されてしまう。(上記にも書いたがそもそもeabihfをまともに解釈しないようだ)

上野動物園に行った

上野動物園に行った。多摩動物公園よりは狭かったけど、数多くの動物がいた。

パンダ

人で賑わっていて辛かった。パンダも奥のほうでほとんど姿を見せず、おたかくとまった動物園のスターといった風情だ。とはいえ愛らしい感じはある。

アジアゾウ

多摩に比べて若々しく、よく動いていた。

インドライオン

見えにくい位置でずーっとじっとしていた。やる気をかんじない。

トラ

多摩はアムールトラだったが、上野にいたのはスマトラトラだった。近くまできてサービス精神旺盛だった。やっぱりトラかっこいいな。阪神は嫌いだけどトラは好きだ。

ゴリラ

思ったより小さいが、近くにくると迫力があった。

ミナミコアリクイ

ちょこまか動きまわって人間を舐め腐っている。

オグロプレーリードッグ

プレーリー式挨拶を間近でみることができた。かわいらしい外見だが、北米では害獣扱いで掃除機で吸われるらしい。

ホッキョクグマ

ずっと日陰でうずくまっていてやる気をかんじなかった。第三次産業を舐め腐っている。

ハシビロコウ

とても近くでみることができたので大満足。シャッター音に反応したのか、カメラを構えている人を攻撃しているところに遭遇できてとても幸運だった。

カバ・コビトカバ

コビトカバみて、カバってこんなもんなのか、と思っていたら本当?のカバはめっちゃでかかった。あれはすごい。。

クロサイ

ちょうど飼育員が餌を与えているところに遭遇した。サイはなんか好きだ。

イリエワニ

爬虫類館にいた。動かずとも近くでみると迫力がすごい。

総評

スマトラトラ・オグロプレーリードッグハシビロコウ・ゴリラあたりは満足度が高かった。このあたりをウリにしていったらいいと思う。 ホッキョクグマはやる気がなさすぎる。

re-visit kNN and operf/LDC

以前書いたものはシングルスレッド版のRust/OCamlよりも遅かった。これをprofilingしてみる。

  • スペック
$ lscpu                                                [kubo39:knn][git:master]
Architecture:          x86_64
CPU 操作モード:   32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                4
On-line CPU(s) list:   0-3
コアあたりのスレッド数:2
ソケットあたりのコア数:2
Socket(s):             1
NUMA ノード数:     1
ベンダー ID:       GenuineIntel
CPU ファミリー:   6
モデル:             69
Model name:            Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz
...
  • dmdのバージョン
$ dmd --version                                        [kubo39:knn][git:master]
DMD64 D Compiler v2.073.1

コードを2.073.1でも動くように修正。

import std.algorithm;
import std.array;
import std.conv;
import std.range;
import std.stdio;
import std.string;
import std.typecons;
import std.parallelism;

struct LabelPixel
{
    int label;
    int[] pixels;
}

auto slurpFile(string filename)
{
    int count;

    auto lp = File(filename)
        .byLine
        .dropOne
        .tee!(a => count++)
        .map!chomp
        .map!(a => a.to!string.split(","))
        .map!(a => LabelPixel(a[0].to!int, a[1..$].to!(int[])) )
        .array;
    return tuple(lp, count);
}

int distanceSqrt(const ref int[] x, const ref int[] y) pure
{
    return reduce!((a, b) => a + (b[0] - b[1]) * (b[0] - b[1]))(0, x.zip(y));
}

int classify(const ref LabelPixel[] training, const ref int[] pixels) pure
{
    int smallest = int.max;
    int result = void;

    foreach (t; training)
    {
        int tmp = distanceSqrt(t.pixels, pixels);
        if (tmp < smallest)
        {
            smallest = tmp;
            result = t.label;
        }
    }
    return result;
}

void main()
{
    const trainingSet = "trainingsample.csv".slurpFile;
    const validationSample = "validationsample.csv".slurpFile;

    int count(const LabelPixel data) pure
    {
        int num;
        if (classify(trainingSet[0], data.pixels) == data.label)
            num++;
        return num;
    }

    immutable num = taskPool.reduce!"a + b"(
        std.algorithm.map!(count)(validationSample[0]).array);

    writefln("Percentage correct: %f percent",
             num.to!double / validationSample[1].to!double * 100.0);
}

timeコマンドで測ってみる。

  • 最適化なし
$ time ./knn                                           [kubo39:knn][git:master]
Percentage correct: 94.400000 percent
./knn  110.16s user 0.03s system 99% cpu 1:50.33 total

うーん、遅い。

  • 最適化あり
$ time ./knn                                           [kubo39:knn][git:master]
Percentage correct: 94.400000 percent
./knn  90.50s user 0.06s system 99% cpu 1:30.59 total

少し早くなったけどぜんぜん遅い。

Rustを見直し。

Rustのシングルスレッド版を再度測ってみる。

$ rustc --version                                      [kubo39:knn][git:master]
rustc 1.17.0-nightly (306035c21 2017-02-18)

少々手直しが必要。

use std::io::{BufRead, BufReader};
use std::fs::File;
use std::path::Path;
use std::str::FromStr;

struct LabelPixel {
    label: i32,
    pixels: Vec<i32>
}

fn slurp_file(file: &Path) -> Vec<LabelPixel> {
    BufReader::new(File::open(file).unwrap())
        .lines()
        .skip(1)
        .map(|line| {
            let line = line.unwrap();
            let mut iter = line.trim()
                .split(',')
                .map(|x| i32::from_str(x).unwrap());

            LabelPixel {
                label: iter.next().unwrap(),
                pixels: iter.collect()
            }
        })
        .collect()
}

fn distance_sqr(x: &[i32], y: &[i32]) -> i32 {
    // run through the two vectors, summing up the squares of the differences
    x.iter()
        .zip(y.iter())
        .fold(0, |s, (&a, &b)| s + (a - b) * (a - b))
}

fn classify(training: &[LabelPixel], pixels: &[i32]) -> i32 {
    training
        .iter()
        // find element of `training` with the smallest distance_sqr to `pixel`
        .min_by_key(|p| distance_sqr(p.pixels.as_slice(), pixels)).unwrap()
        .label
}

fn main() {
    let training_set = slurp_file(&Path::new("trainingsample.csv"));
    let validation_sample = slurp_file(&Path::new("validationsample.csv"));

    let num_correct = validation_sample.iter()
        .filter(|x| {
            classify(training_set.as_slice(), x.pixels.as_slice()) == x.label
        })
        .count();

    println!("Percentage correct: {}%",
             num_correct as f64 / validation_sample.len() as f64 * 100.0);
}

releaseビルド。めちゃくちゃ早い。。

$ cargo build --release                              [kubo39:knnrs][git:master]
   Compiling knnrs v0.1.0 (file:///home/kubo39/dev/dlang/knn/knnrs)
    Finished release [optimized] target(s) in 2.6 secs
$ time ./target/release/knnrs                        [kubo39:knnrs][git:master]
Percentage correct: 94.39999999999999%
./target/release/knnrs  1.21s user 0.01s system 99% cpu 1.217 total

D言語に戻る。

計測しやすいようにシングルスレッド版からはじめる。

import std.algorithm;
import std.array;
import std.conv;
import std.range;
import std.stdio;
import std.string;
import std.typecons;

struct LabelPixel
{
    int label;
    int[] pixels;
}

auto slurpFile(string filename)
{
    int count;

    auto lp = File(filename)
        .byLine
        .dropOne
        .tee!(a => count++)
        .map!chomp
        .map!(a => a.to!string.split(","))
        .map!(a => LabelPixel(a[0].to!int, a[1..$].to!(int[])) )
        .array;
    return tuple(lp, count);
}

int distanceSqrt(const ref int[] x, const ref int[] y) pure
{
    return reduce!((a, b) => a + (b[0] - b[1]) * (b[0] - b[1]))(0, x.zip(y));
}

int classify(const ref LabelPixel[] training, const ref int[] pixels) pure
{
    int smallest = int.max;
    int result = void;

    foreach (t; training)
    {
        int tmp = distanceSqrt(t.pixels, pixels);
        if (tmp < smallest)
        {
            smallest = tmp;
            result = t.label;
        }
    }
    return result;
}

void main()
{
    const trainingSet = "trainingsample.csv".slurpFile;
    const validationSample = "validationsample.csv".slurpFile;

    int count(const LabelPixel data) pure
    {
        int num;
        if (classify(trainingSet[0], data.pixels) == data.label)
            num++;
        return num;
    }

    immutable num = reduce!"a + b"(
        std.algorithm.map!(count)(validationSample[0]).array);

    writefln("Percentage correct: %f percent",
             num.to!double / validationSample[1].to!double * 100.0);
}

シングルスレッド版のほうが早い。といってもあまり変わらないが。

$ dmd -O knn.d                                         [kubo39:knn][git:master]
$ time ./knn                                           [kubo39:knn][git:master]
Percentage correct: 94.400000 percent
./knn  88.90s user 0.02s system 99% cpu 1:28.94 total

dmdだとだめそう。ldc2は理論値性能が出る?

http://leonardo-m.livejournal.com/111598.html

$ ldc2 -version | head -2
LDC - the LLVM D compiler (1.0.0):
  based on DMD v2.071.2 and LLVM 3.8.1

最適化つきで計測。DMDよりぜんぜん速いけどRustより遅い。

$ ldc2 -O knn.d
$ time ./knn
Percentage correct: 94.400000 percent
./knn  10.76s user 0.02s system 99% cpu 10.794 total

operf

operfはdwarf情報を使うので-gつけてビルド。

$ ldc2 -O -g knn.d
$ sudo operf ./knn
operf: Profiler started
Percentage correct: 94.400000 percent

Profiling done.

opannotate --source をみると、distanceSqrt関数が14%を占めていることがわかる。

$ opannotate --source
[ ... ]
/* 
 * Total samples for file : "/home/kubo39/dev/dlang/knn/knn.d"
 * 
 *  45735 15.2286
 */


               :import std.algorithm;
               :import std.array;
               :import std.conv;
               :import std.range;
               :import std.stdio;
               :import std.string;
               :import std.typecons;
               :
               :struct LabelPixel
               :{
               :    int label;
               :    int[] pixels;
               :}
               :
               :auto slurpFile(string filename)
               :{
               :    int count;
               :
               :    auto lp = File(filename)
               :        .byLine
               :        .dropOne
               :        .tee!(a => count++)
               :        .map!chomp
               :        .map!(a => a.to!string.split(","))
               :        .map!(a => LabelPixel(a[0].to!int, a[1..$].to!(int[])) )
               :        .array;
               :    return tuple(lp, count);
               :}
               :
               :int distanceSqrt(const ref int[] x, const ref int[] y) pure
               :{
 45028 14.9931 :    return reduce!((a, b) => a + (b[0] - b[1]) * (b[0] - b[1]))(0, x.zip(y)); /* _D3knn12distanceSqrtFNaKxAiKxAiZi total: 165424 55.0818 */
               :}
               :
               :int classify(const ref LabelPixel[] training, const ref int[] pixels) pure
               :{
               :    int smallest = int.max;
               :    int result = void;
               :
   523  0.1741 :    foreach (t; training)
               :    {
    33  0.0110 :        int tmp = distanceSqrt(t.pixels, pixels);
    50  0.0166 :        if (tmp < smallest)
               :        {
               :            smallest = tmp;
               :            result = t.label;
               :        }
               :    }
               :    return result;
               :}
               :
               :void main()
               :{
               :    const trainingSet = "trainingsample.csv".slurpFile; /* _Dmain total:    711  0.2367 */
               :    const validationSample = "validationsample.csv".slurpFile;
               :
               :    int count(const LabelPixel data) pure
               :    {
               :        int num;
   101  0.0336 :        if (classify(trainingSet[0], data.pixels) == data.label)
               :            num++;
               :        return num;
               :    }
               :
               :    immutable num = reduce!"a + b"(
               :        std.algorithm.map!(count)(validationSample[0]).array);
               :
               :    writefln("Percentage correct: %f percent",
               :             num.to!double / validationSample[1].to!double * 100.0);
               :}

distanceSqrtの実装を変えてみる。

...
int distanceSqrt(const ref int[] x, const ref int[] y) pure
{
    int total;
    foreach (i, a; x)
        total += (a - y[i]) ^^ 2;
    return total;
}
...

4倍以上早くなった。

$ ldc2 -O knn.d
$ time ./knn
Percentage correct: 94.400000 percent
./knn  2.45s user 0.01s system 99% cpu 2.463 total

その他シングルスレッド版では無駄な処理を省いてみる。

import std.algorithm;
import std.array;
import std.conv;
import std.range;
import std.stdio;
import std.string;
import std.typecons;

struct LabelPixel
{
    int label;
    int[] pixels;
}

auto slurpFile(string filename)
{
    int count;

    return File(filename)
        .byLine
        .dropOne
        .map!chomp
        .map!(a => a.to!string.split(","))
        .map!(a => LabelPixel(a[0].to!int, a[1..$].to!(int[])) )
        .array;
}

int distanceSqrt(const ref int[] x, const ref int[] y) pure
{
    int total;
    foreach (i, a; x)
        total += (a - y[i]) ^^ 2;
    return total;
}

int classify(const ref LabelPixel[] training, const ref int[] pixels) pure
{
    int smallest = int.max;
    int result = void;

    foreach (t; training)
    {
        int tmp = distanceSqrt(t.pixels, pixels);
        if (tmp < smallest)
        {
            smallest = tmp;
            result = t.label;
        }
    }
    return result;
}

void main()
{
    const trainingSet = "trainingsample.csv".slurpFile;
    const validationSample = "validationsample.csv".slurpFile;

    immutable num = validationSample
        .filter!(a => classify(trainingSet, a.pixels) == a.label)
        .count;

    writefln("Percentage correct: %f percent",
             num.to!double / validationSample.length.to!double * 100.0);
}

うーむ、誤差程度。というか少し遅くなってる。

$ ldc2 -O knn.d
$ time ./knn
Percentage correct: 94.400000 percent
./knn  2.52s user 0.00s system 99% cpu 2.528 total

もう一度operfをかけてみる。72%が二乗の処理らしい。

...
               :int distanceSqrt(const ref int[] x, const ref int[] y) pure
               :{
               :    int total;
  8513 13.5856 :    foreach (i, a; x)
 45571 72.7251 :        total += (a - y[i]) ^^ 2;
               :    return total;
               :}
...

operf –assemblyもしてみたがあまりわからなかった。

sub -> imul -> add -> inc の場所で60%以上喰ってるらしいが。。

000000000001a580 <_D3std9algorithm9iteration62__T12FilterResultS213knn4mainFZ9__lambda1TAxS3knn10LabelPixelZ12FilterResult8popFrontMFNaZv>: /* _D3std9algorithm9iteration62__T12FilterResultS213knn4mainFZ9__lambda1TAxS3knn10LabelPixelZ12FilterResult8popFrontMFNaZv total:  54369 86.7655 */
...
  4071  6.4968 :   1a600:   cmp    %rsi,%rbx
    21  0.0335 :   1a603:   jae    1a649 <_D3std9algorithm9iteration62__T12FilterResultS213knn4mainFZ9__lambda1TAxS3knn10LabelPixelZ12FilterResult8popFrontMFNaZv+0xc9>
  3182  5.0780 :   1a605:   mov    (%rcx,%rbx,4),%r8d
 14565 23.2438 :   1a609:   sub    (%rdx,%rbx,4),%r8d
  6849 10.9301 :   1a60d:   imul   %r8d,%r8d
 10065 16.0624 :   1a611:   add    %r8d,%ebp
  9923 15.8358 :   1a614:   inc    %rbx
  5268  8.4070 :   1a617:   cmp    %rax,%rbx
    35  0.0559 :   1a61a:   jb     1a600 <_D3std9algorithm9iteration62__T12FilterResultS213knn4mainFZ9__lambda1TAxS3knn10LabelPixelZ12FilterResult8popFrontMFNaZv+0x80>
...

そもそもなんでRustこんなはやいのか。

objdumpでみてみる。なるほど、SIMDでループをベクトル化してるのか。releaseビルドぱない。

    // run through the two vectors, summing up the squares of the differences
    x.iter()
        .zip(y.iter())
        .fold(0, |s, (&a, &b)| s + (a - b) * (a - b))
    8b80:       f3 0f 6f 0c b7          movdqu (%rdi,%rsi,4),%xmm1
    8b85:       f3 0f 6f 14 b2          movdqu (%rdx,%rsi,4),%xmm2
    8b8a:       66 0f fa ca             psubd  %xmm2,%xmm1
    8b8e:       66 0f 70 d1 f5          pshufd $0xf5,%xmm1,%xmm2
    8b93:       66 0f f4 c9             pmuludq %xmm1,%xmm1
    8b97:       66 0f 70 c9 e8          pshufd $0xe8,%xmm1,%xmm1
    8b9c:       66 0f f4 d2             pmuludq %xmm2,%xmm2
    8ba0:       66 0f 70 d2 e8          pshufd $0xe8,%xmm2,%xmm2
    8ba5:       66 0f 62 ca             punpckldq %xmm2,%xmm1
    8ba9:       66 0f fe c8             paddd  %xmm0,%xmm1
    8bad:       f3 0f 6f 44 b7 10       movdqu 0x10(%rdi,%rsi,4),%xmm0
    8bb3:       f3 0f 6f 54 b2 10       movdqu 0x10(%rdx,%rsi,4),%xmm2
    8bb9:       66 0f fa c2             psubd  %xmm2,%xmm0
    8bbd:       66 0f 70 d0 f5          pshufd $0xf5,%xmm0,%xmm2
    8bc2:       66 0f f4 c0             pmuludq %xmm0,%xmm0
    8bc6:       66 0f 70 c0 e8          pshufd $0xe8,%xmm0,%xmm0
    8bcb:       66 0f f4 d2             pmuludq %xmm2,%xmm2
    8bcf:       66 0f 70 d2 e8          pshufd $0xe8,%xmm2,%xmm2