K&Rを読もう(2) 1.2 - 1.3 温度変換

K&Rを見たとき、何処かで見た事のあるプログラムが多数存在する。温度変換もそのひとつ。正規表現のバイブル「ふくろう本」の例題でも使われている(読んでないけど)

由緒正しき温度変換をボクノスなりに読み解こうと思う。

整数の割り算

温度変換のポイントは整数の割り算だと思う。

1/2をやろう。

printf("%d\n", 1 / 2); /* 0 */

結果は0。整数演算では結果も整数になる。なぜなら、「整数と浮動少数では扱うレジスタが別だから」である。

0.5を表示させるときは、

printf("%f\n", 1.0 / 2.0); /* 0.50000 */

とする。

バグバグなprintf

変なの出てきた。

printf("%f\n", 1 / 2); /* -0.234262 */

うは、ひでぇ。出てくる数値はバラバラになる。

「整数と浮動少数では扱うレジスタが別だから」というのを確認しよう。

原因を探る

何故正しく表示されないのか原因を探る。

printfで単純に1を表示させる。

printf("%d\n", 1);
printf("%f\n", 1.0);

gcc -g付きでコンパイルして、objdump -dSで逆アセンブルする。

    printf("%d\n", 1);
 8048395:       c7 44 24 04 02 00 00    movl   $0x2,0x4(%esp)
 804839c:       00
 804839d:       c7 04 24 c8 84 04 08    movl   $0x80484c8,(%esp)
 80483a4:       e8 0f ff ff ff          call   80482b8 <printf@plt>
    printf("%f\n", 1.0);
 80483a9:       dd 05 d0 84 04 08       fldl   0x80484d0
 80483af:       dd 5c 24 04             fstpl  0x4(%esp)
 80483b3:       c7 04 24 cc 84 04 08    movl   $0x80484cc,(%esp)
 80483ba:       e8 f9 fe ff ff          call   80482b8 <printf@plt>

fldlは定数のロード、fstpでpush。

出てくるバイナリは全く別。

浮動少数の場合、扱うレジスタが別であることが確認できる。

温度変換

では、温度変換プログラムしよう。まずは結果。

    fahr celsius
       0     -17
     100      37
     200      93
     300     148

     300     148
     200      93
     100      37
       0     -17

       0   -17.8
     100    37.8
     200    93.3
     300   148.9

 celsius    fahr
       0    32.0
     100   212.0
     200   392.0
     300   572.0

SICP中なので、関数ポインタを多用する。

#include <stdio.h>
#include <stdlib.h>

typedef void (*proc_t)(int);

static void display_columun(char *a, char *b); 

static void iter(int start, int end, int step, proc_t proc);
static void reverse(int start, int end, int step, proc_t proc);

static void fahr_to_int_celsius(int fahr);
static void fahr_to_float_celsius(int fahr);
static void celsius_to_float_fahr(int celsius);

int main(void)
{
    int start = 0;
    int end = 300;
    int step = 100;

    display_columun("fahr", "celsius");

    /* F to int C */
    iter(start, end, step, (proc_t) fahr_to_int_celsius);
    puts("");

    /* F to int C (reverse)*/
    reverse(end, start, step, (proc_t) fahr_to_int_celsius);
    puts("");

    /* F to float C */
    iter(start, end, step, (proc_t) fahr_to_float_celsius);
    puts("");

    display_columun("celsius", "fahr");

    /* C to float F */
    iter(start, end, step, (proc_t) celsius_to_float_fahr);

    exit(EXIT_SUCCESS);
}


static void display_columun(char *a, char *b)
{
    printf("%8s%8s\n", a, b);
}


static void iter(int start, int end, int step, proc_t proc)
{
    int i;
    for (i = start; i <= end; i += step)
        proc(i);
}

static void reverse(int start, int end, int step, proc_t proc)
{
    int i;
    for (i = start; i >= end; i -= step)
        proc(i);
}


static void fahr_to_int_celsius(int fahr)
{
    printf("%8d%8d\n", fahr, 5 * (fahr - 32) / 9);
}

static void fahr_to_float_celsius(int fahr)
{
    printf("%8d%8.1f\n", fahr, 5 * ((float) fahr - 32) / 9);
}

static void celsius_to_float_fahr(int celsius)
{
    printf("%8d%8.1f\n", celsius, (float) celsius * 9 / 5 + 32);
}

もっと抽象化出来る気がする。

まとめと課題

  • レジスタを意識して、キャストしよう。
  • 関数ポインタプログラミングにはまだまだ修行が必要そうだ。
  • FPU命令を覚えたいと思う。

参考

x86アセンブラ入門

GCCで末尾再帰の最適化メモ。

忘れそうなのでメモメモ。

gccで末尾再帰の最適化をしたい場合は、

% gcc -foptimize-sibling-calls

最適化オプション-O2以上でも有効化出来る。

% gcc -O2

安心して再帰が使えるようになりそうです。