前回のstrxfrm関数で使ってしまった関係上、今回は急遽memcpy関数を取り上げることにしました。こういう単純関数ほど、工夫する価値が結構あったりします。今回もアセンブリ言語を使う一歩手前まで最適化してみたいと思います。
この類の関数は、ループの中をどれだけ軽量化できるか、あるいはループの回数をどれだけ減らせるかが鍵になります。まずは素直な実装からです。
#include <string.h>
void *memcpy(void * __restrict__ s1, const void * __restrict__ s2, size_t n) { register char *ss1; register const char *ss2;
for (ss1 = s1, ss2 = s2; n != 0; n--) *ss1++ = *ss2++; return s1; }
やっていることはご覧の通りですので、特に説明もいらないでしょう。コンパイル結果を見てみることにしましょう。オプションは、-mh -mint32 -O2 -fomit-frame-pointerです。
_memcpy: mov.l er4,@-er7 mov.l er0,er4 mov.l er2,er0 mov.l er4,er3 .L8: mov.l er0,er0 beq .L7 mov.b @er1+,r2l mov.b r2l,@er3 adds #1,er3 subs #1,er0 bra .L8 .L7: mov.l er4,er0 mov.l @er7+,er4 rts
例によって、H8/300Hのコードであることにご注意ください。上記の.L8から.L7までがループになっています。er3がss1、er1がss2、er0がnに相当するようです。beq .L7はforの条件式の判定です。
これを踏まえた上で、もう一歩踏み込んでみます。
void *memcpy(void * __restrict__ s1, const void * __restrict__ s2, size_t n) { register char *ss1 = s1; register const char *ss2 = s2; register const char *t = ss2 + n;
while (ss2 != t) *ss1++ = *ss2++; return s1; }
今度は、nをデクリメントするのではなく、終端位置を指すポインタを予め作っておいて、その値との比較を行うようにしました。同じ条件でコンパイルすると、次のようになります。
_memcpy: mov.l er4,@-er7 mov.l er0,er4 mov.l er0,er3 mov.l er1,er0 add.l er2,er0 .L7: cmp.l er0,er1 beq .L6 mov.b @er1+,r2l mov.b r2l,@er3 adds #1,er3 bra .L7 .L6: mov.l er4,er0 mov.l @er7+,er4 rts
大体同じですが、subs #1,er0の分だけ、ループの中が1行減っています。たった1命令ですが、ループの回数が増えれば、その数だけ命令数が減少することになります。さらにもう一歩踏み込みます。
void *memcpy(void * __restrict__ s1, const void * __restrict__ s2, size_t n) { register char *ss1 = s1; register const char *ss2 = s2;
if (n != 0) { register const char *t = ss2 + n; do *ss1++ = *ss2++; while (ss2 != t); } return s1; }
今度はdo文を使いました。今までの二つは、ループの最初で条件判定を行っていたので、どうしてもループの中にbeq命令が入っていました。これをループの最後に判定させることで、ループの最後のbra命令をbeq命令とまとめてしまおうというのが狙いです。それでは同じ条件でのコンパイル結果です。
_memcpy: mov.l er4,@-er7 mov.l er0,er4 mov.l er0,er3 mov.l er2,er2 beq .L2 mov.l er1,er0 add.l er2,er0 .L3: mov.b @er1+,r2l mov.b r2l,@er3 adds #1,er3 cmp.l er0,er1 bne .L3 .L2: mov.l er4,er0 mov.l @er7+,er4 rts
狙い通り、ループの中はうんとシンプルになりました。
ここから先の最適化は、これまでバイト単位で書き込んでいたものをワード単位で書き込むようにすることになると思います。しかし、その方法を採用するとなると、H8/300と、H8/300HやH8Sでは別のコードを書かなければ真の最適化ができなくなります。
また、ワード単位の書き込みの場合、最初に境界調整を調べたりする処理が入るので、サイズが小さい場合はかえって効率が低下します。この辺りの落としどころも難しいので、今回はこの程度にしておきたいと思います。
アセンブリ言語を使う場合は、H8にはEEPROM命令というブロック転送の命令があるので、それを使うのがよさそうです。この話は、いずれ機会を改めてできたらと思います。
|