いけむランド

はてダからやってきました

Re:つーこって

面白いなあと思ったので、感想とか疑問点とかまとめておく。



実際に読んだわけではなく、引用部分とそれへのツッコミしか見ずに感想を書く。*1

読んでいてて気になったのが、scalar 変数を表現するのに 「一値」の変数のような書き方をしていること。たしかにひとつの値しか保持しないから 一値なんでしょうが、耳慣れない言葉で面食らいました。

職場ローカルな用語を一般的な用語だと思って使ってしまっているというのはよくあると思う。実際に自分が所属しているところでも a.out くらい素直に a.out って呼べばいいのに...と思っている。

p166
ハードウェア関係の人以外には分かりづらいでしょうが、I/Oを複数行う際、
わずかな遅延を行わなければならないことがあります。アセンブラには
「NOP」(ノーオペ)という、「何もしない」命令があるですが(原文ママ)、
これの代わりで、「同一変数の代入」(a=a;)を行うのです。
この文を、コンパイラが最適化で削除してしまうのです。すると、
タイミングを取ることができなくなります。
このような削除も、最適化の弊害です。

Cのソースレベルできちんとタイミングを取るのは難しいでしょうけど、 にしたって、a=a とかって… あと、NOPって「のっぷ」ぢゃないんでしょうか? 確かに NO OPERATION の略なんですけど(アセンブラによっては NOOPってのもある)。

たしかにクロックレベルでの遅延の制御とかだと nop *2 を入れてタイミングをいじるみたいなのは想像はできるんだけど、それってインラインアセンブラでやるのが一番いいんじゃないのかな?
それに副作用のない文を nop 相当の命令にするために「コンパイラに最適化をさせない」というのは方針としてどうなんだろう?
この文だけ最適化しないという制御が #pragma とかでできるコンパイラならいいけど、普通はコンパイラオプションでファイル単位の粒度なんじゃないの?(gcc しか使ったことがないから何とも言えないけど...。)

p165 最適化されては困る例
制御用では、ハードウェアとの入出力を行う処理と、そのデータに基づいて
動作する処理は別々であることが多いです。
他の処理で代入されるフラグが立つまで、遅延する処理を考えます。
時間については不問です。

#define READY_FLAG (0x80U)
extern unsigned short us_reg;
while (1) {
  if((us_reg & READY_FLAG) != 0) break;
}

この文を、コンパイラが「繰り返し回数の最適化」によって、判定文を
繰り返しの外に出してしまうのです。

「us_reg」の変更要因がない(左辺での代入がない)から、回す意味なしと判断して、
次のような文として解釈するのです。

if((us_reg & READY_FLAG) != 0) {
  while (1) {
    break;
  }
}

すると、遅延どころか、一度通過して終わってしまう処理に書き換えられたことになります。
これは、困ります。
この例で最適化を極論すると、繰り返し文の意味がなくなるので、判定文すらも意味なしになって、
文そのものがなくなってしまいます。
つまり、実行すべき処理がなくなる…?

いくらなんでも、ifが外に出て本体が空のwhileループになるような最適化をかます コンパイラはないような気がするんですが、世間は広いしなあ。 それはともかく、volatileを使わないのには 何か理由があるんでしょうか ○| ̄|_

手元の gcc で -O つけたら、普通に if 文がループ外に出た。ループ内で条件不変な if 文を外に出したらループ本体が空になるから、それはハードウェアに依存した待ちのためのループと特別扱いして、最適化を抑止するようなコンパイラはさすがに (最近は) ないのではないかと思う。

ちなみに volatile を指定したら、-O を指定していても、予想通りループ外に出なくなった。

#ifdef ENABLE_VOLATILE
#define VOLATILE volatile
#else
#define VOLATILE
#endif

extern VOLATILE int flag;

extern void function()
{
  while (1) {
    if (flag) break;
  }
}
% gcc -S volatile.c
% cat volatile.s
        .file   "volatile.c"
        .text
.globl _function
        .def    _function;      .scl    2;      .type   32;     .endef
_function:
        pushl   %ebp
        movl    %esp, %ebp
L2:
        cmpl    $0, _flag       # ループ内で _flag と 0 を比較している
        je      L2
        popl    %ebp
        ret
% gcc -S -O volatile.c
% cat volatile.s
        .file   "volatile.c"
        .text
.globl _function
        .def    _function;      .scl    2;      .type   32;     .endef
_function:
        pushl   %ebp
        movl    %esp, %ebp
        cmpl    $0, _flag       # ループ外で _flag と 0 を比較している
L2:
        je      L2
        popl    %ebp
        ret
% gcc -S -O volatile.c -DENABLE_VOLATILE
% cat volatile.s
        .file   "volatile.c"
        .text
.globl _function
        .def    _function;      .scl    2;      .type   32;     .endef
_function:
        pushl   %ebp
        movl    %esp, %ebp
L2:
        movl    _flag, %eax     # ループ内で _flag と 0 を比較している
        testl   %eax, %eax      # ループ内で _flag と 0 を比較している
        je      L2
        popl    %ebp
        ret


あと、一言ずつ。

  • ヌル文字のことを NULL って略す人がたまにいるけど、混乱することがあるので止めて欲しいな。><
  • a[i] を *(a+i) と書かないといけない状況に今まで遭遇したことがない。なので、どういう時にこんな書き方をするのか正直わからない。


他にもいろいろとつっこまれているけど、どうも制御のようにかなりハードウェア寄りの C を書いている人はその特定のハードウェアおよびコンパイラ特有のものもすべて C 言語の標準だと思い違いしていることがけっこうある気がする。(著者が必ずしもそうであるとは思わないけど。*3 )

*1:よって、的外れなことを言っている可能性も十分にある。

*2:やっぱり読みは「のっぷ」だと思う。

*3:ただ、前にいた会社はやっぱりそういう「けっこう深いけどちょっと狭すぎる」人はいた。