お題にそってプログラミング

最近、何となくですが、お題にそってプログラミングなんていうのが面白いかな、とか思ってます。
色々な人が居るんだなー、とか色々思ったり。
という事で、色々なパターンで書いてみました。

16進文字列から数値を取り出す関数を作りなさい。
ただし、標準ライブラリは使用してはいけないこととする。strlenも駄目
その代わり、[0-9a-fA-F]以外の不正な文字は入力されないこととする。

では回答編は続きを。

1.馬鹿正直法。

int parsehex1 (char *src) {
  int res = 0;
  int buf = 0;
  char *p;
  for (p = src; *p != 0; p++){
      buf = 0;
      switch (*p){
	case '0':
	  buf = 0;
	  break;
	case '1':
	  buf = 1;
	  break;
	case '2':
	  buf = 2;
	  break;
	case '3':
	  buf = 3;
	  break;
	case '4':
	  buf = 4;
	  break;
      .......
	case 'E':
	case 'e':
	  buf = 14;
	  break;
	case 'f':
	case 'F':
	  buf = 15;
	  break;
	}
      res = res * 16 + buf;
    }
  return res;
}

あり得ない、と思う人も居るかもしれませんが、たまに見ます。
それも中堅ぐらいの人のソースで。
速度重視なのかもしれませんが、どうでしょう。

2.アプリ屋さんにありがちな普通の書き方

int parsehex2 (char *src){
  int res = 0;
  int buf = 0;
  char *p;
  for (p = src; *p != 0; p++){
      buf = 0;
      if (*p <= '9' && *p >= '0'){
	  buf = (int) *p - (int) '0';
	}else if (*p <= 'f' && *p >= 'a'){
	  buf = (int) *p - (int) 'a' + 10;
	}else if (*p <= 'F' && *p >= 'A'){
	  buf = (int) *p - (int) 'A' + 10;
	}
      res = (res << 4) + buf;
    }
  return res;
}

規範的ですね。コレは無難です。
可読性もいいし。

3.メモリ富豪 組み込み屋さん 速度がシビアな人。

char table[] = 
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*00..0F */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*10..1F */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*20..3F */
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0,	/*30..4F */
  0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*40..5F */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*50..6F */
  0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*60..7F */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*80..8F */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*90..9F */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*A0..AF */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*B0..BF */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*C0..CF */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*D0..DF */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,	/*E0..EF */
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };  /*F0..FF なぜ全部用意するかと言うとはみ出さぬ様に。 */

int parsehex3 (char *src){
  int res = 0;
  char *p;
  for (p = src; *p != 0; p++){
      res = (res << 4) + table[(unsigned char) *p];
    }
  return res;
}

昔よく見た手法です。
僕もシビアなときにはたまに使う手法。とにかく早いです。
ただし256バイトのメモリが必要です。

4.解りやすさ派

int parsehex4 (char *src){
  int res = 0;
  int v = 0;
  char *p;
  for (p = src; *p != 0; p++){
      v = (int) *p;
      v -= 0x30;		//0x30=0なので。
      if (v >= 0x10) v -= 7;			/*0x41=A なので、41-30-7 */
      if (v >= 0x10) v -= 0x20;		/*0x61=a なので、41-30-7-20 */
      res = (res << 4) + v;
    }
  return res;
}

これもシンプルですね。
ただ、絶対にifを2回通るので、パフォーマンスが心配です。

5.ifが嫌いすぎて速さを求めてるつもりが逆効果な派

int parsehex5 (char *src){
  int res = 0;
  char *p;
  for (p = src; *p != 0; p++) res = (res << 4) + (int) *p - 0x30 - (*p >= 0x41) * 7 - ((int) *p >=0x61) * 0x20;
  return res;
}

やり過ぎです。
それに掛け算はよろしくない。ifが悪いと思いすぎてて掛け算のコストを全然考えてないです。
MSX-BASICでは速かったんですけどねぇ

6.ちょっと進化したifが嫌いな人

int
parsehex6 (char *src)
{
  int res = 0;
  char *p;
  for (p = src; *p != 0; p++) res = (res << 4) + (int) *p - 0x30 - ((*p >= 0x41) ? 7 : 0) - (((int) *p >= 0x61) ? 0x20 : 0);
  return res;
}

三項演算子使ってるだけ酷い。

7.コンパイラが信じられない派

int parsehexa (char *src){
  int res = 0;
  asm (" movl 8(%ebp),%ecx \n\t"
       "    movl $0, %edx \n\t"
       " movl $0, %eax \n\t"
       " calc_len: \n\t"
       " movl $0,%ebx \n\t"
       " movb (%ecx,%edx),%bl \n\t"
       " cmpb    $0,     %bl \n\t"
       " je      exit \n\t"
       " subb $0x30, %bl \n\t"
       " cmpb $0x10, %bl \n\t"
       " jbe calc \n\t"
       " \n\t"
       " subb $0x07, %bl \n\t"
       " cmpb $0x10, %bl \n\t"
       " jb calc \n\t"
       " \n\t"
       " subb $0x20, %bl \n\t"
       " calc: \n\t"
       " sall $4, %eax \n\t"
       " add %ebx,%eax \n\t"
       " inc %edx \n\t"
       "    jmp     calc_len \n\t"
       " exit: \n\t"
       " movl %eax,-4(%ebp) ");
  return res;
}

コレはやって良い時と悪い時があります。
百万回ぐらい呼ばれるならやってもいいかもです。
ちなみに、動きますが、久々に書いたので怪しいです。

さて、こんな風に色々組んでみましたが、実際の動きはどうでしょう。
テスト用ルーチンはこちら。
sys/timesとか一通りincludeしてください。

/*テスト用 時間計測*/
double
getsec ()
{
  struct timeval tv;
  gettimeofday (&tv, NULL);
  return tv.tv_sec + (double) tv.tv_usec * 1e-6;

}

/*テストのメイン*/
int
main (char *argc, char **argv)
{
  char *test = "A2cdEfbc";
  int (*p[7]) (char *src) =
  {
  parsehex1, parsehex2, parsehex3, parsehex4, parsehex5, parsehex6,
      parsehexa};
  char n[7][10] =
    { "parsehex1", "parsehex2", "parsehex3", "parsehex4", "parsehex5",
"parsehex6", "parsehexa" };
  int i = 0;
  long j = 0;
  unsigned int tmp = 0;
  int max = 10000000;
  printf ("test char ->%s\n", test);
  for (i = 0; i < 7; i++)
    {
      printf ("output test...%d (%s)", i, &n[i]);
      tmp = (*p[i]) (test);
      printf ("%u \n", tmp);
    }
  for (i = 0; i < 7; i++)
    {
      double start = getsec ();
      printf ("loop %d start time (%s)...", i, &n[i]);
      for (j = 0; j < max; j++)
	{
	  tmp = (*p[i]) (test);
	}
      double sec = getsec () - start;
      printf ("%f sec )\n", sec);
    }
  return 0;
}

結果がこちら。

test char ->A2cdEfbc
output test...0 (parsehex1)2731405244 
output test...1 (parsehex2)2731405244 
output test...2 (parsehex3)2731405244 
output test...3 (parsehex4)2731405244 
output test...4 (parsehex5)2731405244 	
output test...5 (parsehex6)2731405244 
output test...6 (parsehexa)2731405244 
loop 0 start time (parsehex1)...1.319381 sec )
loop 1 start time (parsehex2)...0.748295 sec )
loop 2 start time (parsehex3)...0.464418 sec )
loop 3 start time (parsehex4)...0.918671 sec )
loop 4 start time (parsehex5)...0.831350 sec )
loop 5 start time (parsehex6)...0.829392 sec )
loop 6 start time (parsehexa)...0.433116 sec )

非力なマシンでやりましたが、やっぱりベタなswitchは遅い。
次は案外、parsehex4が遅いですね。
if嫌い派も遅い。
やっぱりテーブル作って参照するだけ、parsehex3とかはやたら速い。
無難なparsehex2は無難でいいですね。
そしてアセンブラは異常に速い。

ここで問題です。
parsehex4には遅くなる原因が一つあって、少し改造すればもっと速くなります。
さて、どう修正するでしょう。