組み込みくらいでしか気にしないかもしれませんが。
このくらい常識としておかないと先には進めませんよ。
さて、現在一番出回っているCPUはアスロンだろうか、インテルだろうか?という話はさておき、とりあえずインテルのペンティアム4とかそういうやつを対象に話をしてみましょうか。いわゆるIA32ってやつですね。
IA32を知らない人はインテルアーキテクチャの略ですし、32は32bitですよ。
たぶんI386からペンティアム4くらいまでがそう。
CoreまでいくとIA64じゃないかなぁ?まあそんな話はどうでもよし。
さて、32bitCPUですが、32bitを同時に演算できちゃいますよ、すごいです。
32bitっていうと8で割ればByteですから4Byteですね。
つまりは4Byte同時に計算できるんですよ。
さて、計算計算っていってるけどCPUってどうやって計算しているのか。
メモリにあるデータでCPUがアクセスして加算とかしてるんですかね。
あってるけど、ここでの論議では不足です。
まあ、CPU内のレジスタにあるデータで加算とかしてるくらいで抑えておきましょう。
まあつまり、このレジスタの一つ一つが4Byteまではいるメモリになってて、CPUはそれで計算するんですな。
さて、C言語での型を思い出すと、char、short、long、doubleあたりですな。
それぞれ、char=1Byte、short=2Byte、long=4Byte、double=4Byteです。
ま、あえてintを避けたのですが。
intは、そのCPUで最も適した大きさというのが根本的な考えで、つまりは機種依存型なのです。
とか書くとまたひどいことになりそうですが。
要するに、16bitマシンでintって書くと2Byteの型になり、32bitマシンでintって書くと4Byteの型になるのです。
だから私はあまり好きじゃないです。
まー・・・再利用する気のないプログラムで、問題ないとわかっている場合についてのみ、タイピング数が一番すくないintを使うことはあります。
話は戻ります。
この型の違いによるCPU計算速度の差はあるのか?
つまりchar+charだと1Byte+1Byte、long+longだと4Byte+4Byteになります。
桁数4倍ですよ。計算量も4倍っぽいじゃないですか。
これがまあ、同じ速度なんですよね。
32bitマシンってそういうことですよ。
これが大前提。
やっぱそんな変なことしっても、ただのトリビアで終わったらつまらない。
そういう知識は実用にならないのか?というお話。
って書くくらいだからまあ聞いとくれ。
void copy(void)
{
char table[400];
for(int i=0; i!=400; i++ ){
table[i] = 1;
}
}
なんのテーブルかよくわからんけど、まあ400個、「1」の入ったテーブルが欲しいんだろう。
これ、ループ回数みても400回だよね、すごい時間かかるよね。
下の見てよ。
ちょっとhtmlの関係上、評価式が気に入らないかもしれないけど、仕様記述論的に見るとわりと正解なので許して・・・
void copy(void)
{
char table[400];
int *tablePointer = table;
for(int i=0; i!=100; i++ ){
*tablePointer = 0x01010101;
tablePointer++;
}
}
わかりずらいよね、でもループ回数100回で前より4倍早くなってるよ。
つまりはそういうことなんですな。
補足として。
下はポインタ移動で計算行ふえてて、計算回数でみれば400から200になっただけじゃん!とか思うかもしれない。
その人は惜しい、けれどまだ初心者から抜けられていない。
配列table[i]というのは、tableの先頭からiだけ移動するために毎回演算を行う。
このため上の「table[i] = 1」については、アドレスを求める演算と1を代入する演算の2回を行っている。
よって下とあまり変わらない。
さらに機械語の観点からみればポインタ演算のほうが早かったりして、うんぬんかんぬん。
まあ下が計算回数200だというならば、上は計算回数800なのだ。
さて、このアーキテクチャはメモリアクセスにも密着な感じ。
CPUが計算するにはレジスタを使うけど、すべてのデータがレジスタに入っているわけじゃない。
むしろ現在使用しているデータのうちのほとんどはメインメモリに入っている。
CPUはデータをメインメモリからレジスタに写し、演算をおこなう。
さてレジスタは4Byte、とりあえず4Byte読み込んじゃって、charとかだったらそこから切り取れ。
じゃメインメモリにアクセスするのも4Byte単位でいいんじゃないの。
4の倍数のアドレス+4Byte分でメモリを操作しちゃえ。
という感じ。
これこそがバウンダリつまり境界です。
4の倍数アドレスにおける境界ができているのです。
こいつがまた様々な問題を引き起こします。
先ほどのように速度のために宣言とは違う型でアクセスする場合があります。
たとえば下記のようにchar配列にさまざまなデータを、セットで管理している場合とか。
このようなデータ構造はよく通信やゲームで使用します。
byte 2 2 4
│type│length│data │
└──┴───┴─────────┘
そうするとtypeとlength部分はshortで、データ部分はlongでアクセスできます。
これが下記のようなデータ構造になると、とたんに例外でプログラムはとまってしまいます。
byte 2 4 2
│type│data │length│
└──┴─────────┴───┘
longでアクセスするさいに、data開始アドレスが4の倍数になっていないことが問題です。
dataはアドレスが4で割り切れる場所におかなければなりません。
ちなみにshortなら2で割り切れる場所におかなければなりません。
アセンブラとレジスタを理解すれば話は簡単。
まず、レジスタ。
32bitレジスタ、とはいうものの一つ一つは16bitで分かれている。
16bitレジスタのうち片方、数値でいえば下位にあたるほうが8bitづつ分かれている。
アセンブラにおいて、これらレジスタの部位にあたるものを含めてレジスタ名のように扱う。
32bitのレジスタであるEAXレジスタの、下位16bitを示す場合AXという名前で表し、さらにその下位8bitはAXLという名前で表す。
LというのはLowのことで、上位8bitはHighからAXHとなる。
図にすると下記の感じ。
│ EAX │ │ AX │ │ │ AXL │ AXH │ │ └───┴───┴───────┘
C言語でいえば、EAXがいわゆるlong、IA32だとintになります。
shortがAX、charがAXLとAXHですね。
メモリアクセスは、EAXレジスタが、4の倍数アドレスへのアクセスをします。
ここから型の違いによって、それぞれの部位を使って値の切り分けをします。
また、上位16bit側には名称がないので、そのままだと使用できません。
これは16bit分、下位へシフト演算し、値をずらすことによって取得できるようにします。
これにより、シフトしなければならない位置にある値は、値の取得だけで演算回数が一回多くなり、処理に時間がかかることになります。
これが、4の倍数のアドレスに値をおくと処理速度が上がることがあるといわれる所以です。
また、これらの出来事により、shortによる奇数アドレスアクセスの禁止、longによる4の倍数以外のアドレスアクセスへの禁止の理由もわかります。
めんどくさい話はまだ続きます。
下記を見てください。
struct TYPE_ZERO{
char m_a;
long m_b;
short m_c;
};
こんなものを用意してみました。
それが何だと言う前に先ほどの話を思い出してください。
longは4の倍数アドレスでしかアクセスできないということは、4の倍数アドレスにしかにしか置けないのです。
ではこの構造体の大きさはいくつなのでしょうか?
変数は1Byteと4Byteと2Byteを使用しています。
足し算をすれば7Byteですけれど、実際には12Byteの大きさになります。
charのあとlongがきたことで、空白の3Byteが作られるのです。
これをpaddingといいます。
padというのは梱包材とか詰め物とか、そういう意味です。
この構造体を下記のように改良すればどうでしょう。
struct TYPE_ZERO{
char m_a;
short m_c;
long m_b;
};
じつはサイズが8Byteになります。
shortへのアクセスが少しだけ遅くなりますけれど・・・ね。
まあ、組み込みくらいしか気にしないかなぁ・・・このへんは。
OS作る人とか、デバドラ造る人とか・・・かな。
ま、C言語はハードウェアアーキテクチャに密接に絡むし、アセンブラ知ってる人と知らない人で格の違うプログラムになるということが解ってもらえればいいわけだが。
C言語を高級言語とする本は、とりあえず購入見合わせていいかも。
そもそも何が高級でなにが・・・って話からなのかなぁ。