CPUやOSのbit数
64bit CPUが出てきてずいぶん経ちますがその前は32 bit CPUが全盛期でした。
その前は16 bit CPUでその前は8 bit CPUでした。(Z8は懐かしい…)
今後128 bit CPUが一般的になるのかどうかは微妙かもしれませんが、今のAIとかの流れを見ると128 bitが出るよりもGPUで計算することが主流なのかもしれません。ただ128 bit CPUが一般的に使われるようになることはないとは言えないのかと思います。
組み込みでbit違いのOS間の通信
さてなぜCPUのbit数の話をしたかというと、以前32 bit CPU <-> 32 bit CPUの間にDPRAM(Dual Port RAM)を配置していたシステムを64 bit CPU(OS) <-> 32 bit CPUの間にDPRAMを配置したシステムに移植した際に起きた問題と対策のメモです。
きっとUARTとかで通信するときもstructで宣言したものを使用したら同じことが起きるのだと思います。
環境
32bit OS
$ uname -a
Linux Debian-32 6.1.0-23-686-pae #1 SMP PREEMPT_DYNAMIC Debian 6.1.99-1 (2024-07-15) i686 GNU/Linux
$ gcc --version
gcc (Debian 12.2.0-14) 12.2.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
64bit OS
$ uname -a
Linux c-on-linux 6.8.0-39-generic #39-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul 5 21:49:14 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
実験
以下の3つの場合を試してみます。
#include <stdint.h>
struct TestStructA
{
char c_val;
uint16_t u16_val;
uint32_t u32_val;
uint64_t u64_val;
};
struct TestStructB
{
char c_val;
uint32_t u32_val;
uint16_t u16_val;
uint64_t u64_val;
};
struct TestStructC
{
char c_val;
uint64_t u64_val;
uint32_t u32_val;
uint16_t u16_val;
};
実験するプログラムは
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "Struct.h"
/*実験時はTEST_STRUCTを変えて試していきます。*/
#define TEST_STRUCT TestStructA
void PrintStruct ( struct TEST_STRUCT input)
{
char *p;
int i;
printf("Struct sizeof : %zd \n",sizeof(struct TEST_STRUCT));
printf("c_val = %X \n",input.c_val);
printf("u16_val = %"PRIX16"\n",input.u16_val);
printf("u32_val = %"PRIX32"\n",input.u32_val);
printf("u64_val = %"PRIX64"\n",input.u64_val);
/*Structの実際のメモリ配置を表示*/
p = (char *)&input;
for ( i = 0 ; i < sizeof(struct TEST_STRUCT) ; i++ ){
if (( i % 0x10 ) == 0 ){
printf("\n %04X : ",i);
}
printf(" %02X",*p&0xFF);
p++;
}
printf("\n");
}
int main ( int argc , char **argv )
{
struct TEST_STRUCT test;
/*どこの値が変わったのかわかるように一旦0xFFで埋めます。*/
memset ( &test,0xFF,sizeof(struct TEST_STRUCT));
/*testの値を書き込んで表示します*/
test.c_val = 0x12;
test.u16_val = 0x1234;
test.u32_val = 0x12345678;
test.u64_val = 0x123456789ABCDEF;
PrintStruct(test);
return 0;
}
(32bitでコンパイルするときはワーニングが出ます。)
64bit OSで
- StructA = outA
- StructB = outB
- StructC = outC
としたときの結果
$ ./outA
Struct sizeof : 16
c_val = 12
u16_val = 1234
u32_val = 12345678
u64_val = 123456789ABCDEF
0000 : 12 FF 34 12 78 56 34 12 EF CD AB 89 67 45 23 01
$ ./outB
Struct sizeof : 24
c_val = 12
u16_val = 1234
u32_val = 12345678
u64_val = 123456789ABCDEF
0000 : 12 FF FF FF 78 56 34 12 34 12 FF FF FF FF FF FF
0010 : EF CD AB 89 67 45 23 01
$ ./outC
Struct sizeof : 24
c_val = 12
u16_val = 1234
u32_val = 12345678
u64_val = 123456789ABCDEF
0000 : 12 FF FF FF FF FF FF FF EF CD AB 89 67 45 23 01
0010 : 78 56 34 12 34 12 FF FF
32bit OSでは
$ ./outA
Struct sizeof : 16
c_val = 12
u16_val = 1234
u32_val = 12345678
u64_val = 123456789ABCDEF
0000 : 12 FF 34 12 78 56 34 12 EF CD AB 89 67 45 23 01
$ ./outB
Struct sizeof : 20
c_val = 12
u16_val = 1234
u32_val = 12345678
u64_val = 123456789ABCDEF
0000 : 12 FF FF FF 78 56 34 12 34 12 FF FF EF CD AB 89
0010 : 67 45 23 01
$ ./outC
Struct sizeof : 20
c_val = 12
u16_val = 1234
u32_val = 12345678
u64_val = 123456789ABCDEF
0000 : 12 FF FF FF EF CD AB 89 67 45 23 01 78 56 34 12
0010 : 34 12 FF FF
この結果を見るとStructAはメモリダンプの値が同じなので問題なく動くと思いますが、StructBとStructCはダンプした値が違うのでうまく通信ができないことになります。
structの中のデータの配置によりますが、ほとんど大丈夫なんだけど一か所だけデータが化けるんだけど・・・。
というのは問題の原因の推測がとても難しいです。特に今回のようにソースコード的には問題ないのは難しいと思います。
対策
そもそもstructの部分は両方のCPUで共有することで、内容が変更されたときに相手側の変更漏れを防ぐというのが最大のポイントだと思います。
そこで以下のような対策を行いました。
#include <stdint.h>
struct TestStruct
{
char c_val;
char RsvChar[7];
uint32_t u32_val;
uint32_t RsvUint32[1];
uint16_t u16_val;
uint16_t RsvUint16[3];
uint64_t u64_val;
};
char, uint16_t, uint32_tを使うときには64bitのアライメントを取るようにしました。
結果的にstructのサイズ自体は大きくなってしまいますが、データの切れ目を考えて実装する必要はなくなるはずです。
念のため実際に試してみました。
64 bit osの時
$ ./out
Struct sizeof : 32
c_val = 12
u16_val = 1234
u32_val = 12345678
u64_val = 123456789ABCDEF
0000 : 12 FF FF FF FF FF FF FF 78 56 34 12 FF FF FF FF
0010 : 34 12 FF FF FF FF FF FF EF CD AB 89 67 45 23 01
32 bit osの時
$ ./out
Struct sizeof : 32
c_val = 12
u16_val = 1234
u32_val = 12345678
u64_val = 123456789ABCDEF
0000 : 12 FF FF FF FF FF FF FF 78 56 34 12 FF FF FF FF
0010 : 34 12 FF FF FF FF FF FF EF CD AB 89 67 45 23 01
同じ結果になりました。
実際では同じ型のものをできるだけまとめてアライメントを取る領域を小さくしたいところですが、できるだけ同じ機能とか内容でまとめたいというのもあると思うので匙加減が難しいですね。
追記
https://qiita.com/takaug/items/8107dc58d3d99e71f0ce#comment-95808fdf110ea02d7a1d
コメントありがとうございます。
勉強不足でalignasの存在を知りませんでした…。
#include <stdalign.h>
struct TestStruct2
{
alignas(1) char c_val;
alignas(4) uint32_t u32_val;
alignas(2) uint16_t u16_val;
alignas(8) uint64_t u64_val;
};
とすると64bit/32bitともに
$ ./out2
Struct sizeof : 24
c_val = 12
u16_val = 1234
u32_val = 12345678
u64_val = 123456789ABCDEF
0000 : 12 FF FF FF 78 56 34 12 34 12 FF FF FF FF FF FF
0010 : EF CD AB 89 67 45 23 01
となりました。
まとめ
bit数違いのOS間の通信の場合は特に注意が必要だが、私は普段structとかを何も考えずに書いていますが、memcpyとかで大きなstructのコピーを大量に行っていたりした場合、structの中身を見直してパフォーマンスが上がったりするかもしれない(上の例でいうところのFFの数が多ければ多いほど無駄なコピーが起きているはずなので)ということも時々は気にしたほうがいいのかもしれないです。