CodeIQ:構造体のアライメント

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「構造体のアライメント」(CodeIQ)を参照してください。

問題の概要

問題を引用します。
【概要】
構造体の中に、いくつかの要素が入っています。
最後の要素の先頭アドレスが、構造体の先頭から何バイトあとにあるのかを計算して下さい。
ただし、構造体の先頭アドレスは(幸運なことに)16の倍数になっています。

【入出力】
入力は
SDIL
のようになっています。
構造体の中にある要素を示す記号が、アドレス順に区切り文字なしで並んでいます。

各記号の意味は、下表のとおりです:
記号サイズ(バイト数)先頭アドレスの制限
C1なし
S2偶数アドレス
I44の倍数アドレス
L84の倍数アドレス
D88の倍数アドレス
M1616の倍数アドレス
例えば、CMの様になっていた場合、先頭のアドレスにはCが示す要素が入っています。
その後には 15byte のパディング(変数としては使われない詰め物)が入っていて、Mが示す要素はその後に続くことになります。

出力は、SDIL の示す構造体の先頭アドレスから 20 バイト先に末尾の要素があるので
20
のような感じです。
構造体のサイズではなく、最後の要素の先頭アドレスであることに注意して下さい。

【例】
入力出力メモリイメージ(.はパディング)
SDIL20SS......DDDDDDDDIIIILLLLLLLL
CM16C...............MMMMMMMMMMMMMMMM
CSILDC24C.SSIIIILLLLLLLLDDDDDDDDC

【補足】
不正な入力に対処する必要はありません。
Lのアライメントは 4 の倍数です。8 の倍数ではありません。
データのリオーダーはないものとします。
入力文字列の長さは、1 文字以上 16 文字以下です。
実際のアライメントについてはWikipedia の記事が参考になるかもしれません。

私のプログラム

Rubyで解答しています。

#!/usr/bin/ruby

def solve(struct)
	pos = {
		"C" => {"size" => 1, "st" => 1},
		"S" => {"size" => 2, "st" => 2},
		"I" => {"size" => 4, "st" => 4},
		"L" => {"size" => 8, "st" => 4},
		"D" => {"size" => 8, "st" => 8},
		"M" => {"size" => 16, "st" => 16},
	}

	st = 0
	ed = 0
	struct.split("").each_with_index{|c, i|
		if ed % pos[c]["st"] != 0 then
			st = (ed / pos[c]["st"] + 1) * pos[c]["st"]
		else
			st = ed
		end
		ed = st + pos[c]["size"]
#		printf("st=%d, ed=%d\n", st, ed)
	}

	return st
end

# main
while line = gets
	line.strip!
	next if line.empty?

	p solve(line)
end

解説

特に難しいところはなく、素直にやればできます。

考え方

2番目以降の要素の開始位置をどう計算するかだけです。
一つ前までの要素の終了位置を次の要素の「先頭アドレス制限」で割り、その端数を切り上げた値に「先頭アドレス制限」を掛けた値が開始位置になるので、それに「サイズ」を加えたものが次の終了位置になります。これを繰り返し最後の要素の開始位置が答えになります。

例えばCMの場合、次のようになります。
Cの開始位置は0、終了位置は1。
終了位置1をMの「先頭アドレス制限」で割って端数を切り上げると1なので、1に「先頭アドレス制限」の16を掛けるとMの開始位置は16、終了位置は16+16で32。
Mが最後の要素なので答えは16。

main

入力値をsolve()に渡し、結果を印字します。

solve(n)

問題を解きます。

posは記号に対し、サイズと先頭アドレス制限の組みを値とした連想配列です。
stは開始位置、edは終了位置を保持する変数で初期値は共に0です。

入力値を1文字ずつ取り出します。
edをposの対応する記号の「先頭アドレス制限」で割って割り切れたら開始位置までパディングされないのでそのまま、割り切れなかった場合は「先頭アドレス制限」で割り、その端数を切り上げた値に「先頭アドレス制限」を掛けた値を開始位置にします(16〜20行目)。

終了位置はstに記号に対応するサイズを加えた値に更新します(21行目)。

最後まで処理した時のstが答えになるのでそれを返却します。

雑感

問題とはあまり関係ありませんがC言語の場合、構造体のアライメントを求めるテクニックがあります。次のコードがそれです。
hogeは適当な構造体です。
13行目で0をhogeのポインタにキャストし、14〜19行目で構造体のメンバのアドレスを印字しています。

#include 

struct hoge {
	char a;
	short b;
	int c;
	long d;
	float e;
	double f;
};

int main(void){
	struct hoge* h = (struct hoge*)0;
	printf("a=%p\n", &(h->a));
	printf("b=%p\n", &(h->b));
	printf("c=%p\n", &(h->c));
	printf("d=%p\n", &(h->d));
	printf("e=%p\n", &(h->e));
	printf("f=%p\n", &(h->f));
	return 0;
}
出力は次のようになります。
a=0x0
b=0x2
c=0x4
d=0x8
e=0x10
f=0x18

これは次のような意味です。
まず、ポインタの文脈では0はヌルポインタと解釈されるので13行目はヌルポインタを構造体のポインタにキャストしているわけです。hogeのポインタ変数hには通常確保した構造体の領域の先頭アドレスが保持されますが、それが0ということになります。
ということは各メンバのアドレスを&演算子で取得すると0を基準としたアドレス=先頭から何バイトの場所にあるか、がわかるというわけです。

そもそも、メモリの相対位置でアクセスするようなコードはよろしくないのですが、マクロとかで相対位置を定義するとアーキテクチャが変わった時(例えば、Linuxで32bitから64bitになった時にlongが4バイトから8バイトに変わりました)にずれてしまうので困ったことになります。どうしても相対位置を知る必要がある場合はこのようにして求める必要があります。