星期二, 10月 14, 2014

如何避免 Alignment 問題


這次我們談談七大讓工程師黯然消魂的問題中排名第三的問題, 外號 -- “凌波微步"

現在一些高階語言的程式設計師 (例如 Java, PHP 等等)
應該比較少遇到可怕的 Alignment 的問題了
這也是好事情, 程式語言的設計本來就是應該要讓人類避免犯錯

1. 利用Compiler option 處理大部分的 alignment 的問題
首先要澄清一個觀念,
所謂的 Alignment 這種問題在 32 bits CPU上,
只會發生在讀寫 short (2 bytes) or int (4 bytes)這種資料型態上面,
char (1 byte) 的讀寫是不會有Alignment的問題. (有些曾經遇到過的人, 已經走火入魔了, 以為Alignment 無所不在)

如下面這個範例
typedef struct T_PERSON_TAG {
       char         name[9];
       short         age;
       int             id;
}T_PERSION;


T_PERSION person;

void foo(void) {
       strcpy( person.name, “John”);           //1
       person.age = 22;                     //2
       id = 0512001;                             //3
}

本帖隱藏的內容

//1是不會有問題,
因為讀寫都是以byte為單位,

但是//2 和//3 可能會有問題,
簡單的說現在寫進去的資料, 和將來讀出來的資料可能不一樣 (有沒有嚇到..)  
因為讀寫的位置可能不是 word align (4的倍數) 或是 half-word align (2的倍數) (有仔細看到那個 9 嗎?)

會發生問題的原因是 CPU 在設計的時候基於效能的考量, (設計CPU的人也是好意啊 !)  
會讓系統只能在 word align上讀寫 int 或是 half-word align上讀寫 short.

發生 Alignment 的問題時, 系統不一定會當掉, (怎麼可能這麼剛好..)
有時候會只會讀到不正確的資料,
或是寫資料到不正確的位置. 造成除錯的困難. (再一次重創工程師的心靈)   

通常可以加上 compiler option 強制word or half-word align 來解決.
但是不同compiler 的option 不一樣, 要參考 compiler 手冊才知道  (記得要檢查 Make file)   

上面這範例如果是以word align 編譯之後,
會產生如下的資料結構,

typedef struct T_PERSON_TAG {
           char          name[9];
           char          padding[1];        //4
           short         age;
           char          padding[2];        //5
           int            id;
}T_PERSION;

其中, //4 和 //5 就是 compiler 自動加上的.

雖然上面這樣的資料結構會經由 compiler 的(智慧型)補救把 Alignment的問題解決,
但是卻會浪費記憶體.
每宣告一個 T_PERSON 物件就會浪費 5 個bytes的空間. (在侏儸紀的時代, 這真的粉多耶)
如果宣告的量很大, 例如1000個T_PERSON 陣列,
就會浪費 5000 bytes 的空間. (這樣宣告的工程師保證會被技術長約談)
這樣的資料結構越多, 系統可用的記憶體資源就越少.


2. 手動處理Alignment , 可以節省記憶體
正確的寫法是在宣告 structure 時進行人工的 align. (果然還是要靠人類的智慧)

本帖隱藏的內容

例如上面的例子可以將排列的位置改成如下.
就可以解決浪費記憶體的問題.(傑克! 這真是太神奇了...)  

typedef struct T_PERSON_TAG {
   int           id;
   short       age;
   char        name[9];
}T_PERSION;


3. 使用 type cast 時要注意 alignment 的問題

上面談到 Alignment的問題時,
有提到 compiler 會幫我們處理掉 Alignment的問題.
但是有一種情形是 compiler也無能為力的. 要特別小心. (靠人家的總是要注意...)

本帖隱藏的內容

例如下面這個範例, 是想要從某個影像檔的檔頭 (header) 裡面拿到影像檔的大小.
假設使用的平台是使用 Little Endian 格式, (不知道甚麼是 Little Endian 嗎 ? 那你還能看到現在, 真服了你)  
這裡的假設是 image 資料也是 Little Endian 格式

U8 imgae[1024];

void foo(void) {
       U32 size;

       size = *((U32 *) &image[5]); //1
}

&image[5] 這個位置不一定是word align,
因為 image 這塊記憶體不一定會被放在 word align 的地方,
所以得到的資料不一定正確,
不一定這件事情很可怕,
因為有時候可能是正確的, 這會讓除錯非常的困難. (不然怎麼會讓工程師如此黯然銷魂呢...)   

在處理這類資訊的時候,
比較正確的處理方式是一個 byte 接著一個 byte 來處理資料.
不要使用 type cast 的方式.

例如上面這個範例, 應該要改成下面的用法.,


U32 GET_U32 (U8 *src);
{
        U32 ret_value;
       U8 *dest = (U8 *) &ret_value;

       *dest = *src;
       *(dest + 1) = *(src + 1);
       *(dest + 2) = *(src + 2);
       *(dest + 3) = *(src + 3);

       return ret_value ;
}

void foo(void) {
       U32 size;
       
       size = GET_U32((U8 *) &image[5]); //1
}


Alignment 問題的發生通常是在 raw data 轉換時,
所以遇到需要型態轉變(type cast) 的時候,

要特別注意這樣問題的發生.

沒有留言: