読者です 読者をやめる 読者になる 読者になる

URLエンコード/デコード : C++ : メモリ管理手動

C++ speed

以前に作成したC++によるURLエンコード/デコード関数は、変換先の文字列を格納するためにstd::stringクラスを使っていた。
std::stringを使えば、クラス側がメモリ管理を適切に行ってくれるので楽(バグも生じにくい)なのだが、その分オーバヘッドもある。
URLエンコード/デコードの場合は、必要なメモリ量の上限があらかじめ分かっているので、それを利用してメモリ管理*1を明示的に行うようにコードを修正し、再度処理時間を計測してみた。

参照: mmap_t

////////////////////////////
// ファイル名: url_encode.cc
// g++ -O3 -ourl_encode url_encode.cc /* 以前は、最適化オプションとして-O2を指定していた -> 0.002s高速化*/
// time url_encode 対象ファイル
#include "mmap_t.h"

bool is_safe_char(char c) {  return isalnum(c)||c=='.'||c=='-'||c=='_'||c=='*'; }

char* encode_char_to_hex(char c, char* dist) {
  dist[0]='%';
  dist[1]="0123456789ABCDEF"[(c&0xF0)>>4];
  dist[2]="0123456789ABCDEF"[c&0x0F];
  return dist+2;
}

unsigned url_encode(const char* c, char* dist) {
  const char* head = dist;
  for(; *c!='\0'; c++,dist++) {
    if(is_safe_char(*c)) *dist = *c;
    else if (*c==' ')    *dist = '+';
    else                 dist=encode_char_to_hex(*c,dist);
  }
  *dist='\0';
  return dist-head;
}

#include <iostream>
int main(int argc, char** argv) {
  mmap_t mm(argv[1]);
  char* dist = new char[mm.size*3+1];
  url_encode((const char*)mm.ptr,dist);  // XXX: このmmap_tの使い方には、末尾はNULLである保証がないというバグがある。
  //std::cout << dist;
  delete [] dist;
  return 0;
}

////////////////////////////
// ファイル名: url_decode.cc
// g++ -O3 -ourl_decode url_decode.cc  /* 以前は、最適化オプションとして-O3を指定していた -> こちらは変化無し */
// time url_decode 対象ファイル
#include "mmap_t.h"

static const char table[]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,0,10,11,12,13,14,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,11,12,13,14,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
char decode_hex_to_char(const char* c) {
  // XXX: [0-9a-zA-Z]以外の全ての文字は、0として扱われる (e.g. "%@X" => 0)
  return (table[static_cast<unsigned char>(c[1])]<<4)+
          table[static_cast<unsigned char>(c[2])];
}

unsigned url_decode(const char* c, char* dist) {
  const char* head = dist;
  for(; *c!='\0'; c++,dist++) {
    switch(*c) {
    case '%': *dist = decode_hex_to_char(c); c+=2; break;  // XXX: 末尾に'%'が来る不正な文字列が渡された場合の挙動は未定義(ex. "abc%")
    case '+': *dist = ' ';                         break;
    default:  *dist = *c;                          break;
    }
  }  
  *dist='\0';
  return dist-head;
}

#include <iostream>
int main(int argc, char** argv) {
  mmap_t mm(argv[1]);
  char* dist = new char[mm.size+1];
  url_decode((const char*)mm.ptr,dist);
  //std::cout << dist;
  delete [] dist;
  return 0;
}

コンパイル後のコマンドを、以前と同様に『こころ』*2に対して使ってみたところ、URLエンコードに0.06秒、URLデコードに0.04秒、処理時間を要した。
std::stringを使った場合*3に比べ、エンコードが約4倍、デコードが約2倍、高速化されたことになる。
やはり、こういった高度なメモリ管理が不要なケースでは、ポインタ(メモリ領域)プログラマが明示的に操作できるということは、大きな利点だ*4

*1:単に、必要なメモリ領域をあらかじめまとめて確保しておくだけなので、メモリ管理というほど大げさではないが

*2:Apacheのログは紛失してしまった

*3:コメントにも書いてあるが、URLエンコードの場合は、最適化オプションを変えたことも処理速度に影響を与えている

*4:ちなみに、完全に私見だが、最近はCやC++が他のコンパイラ言語(Java, Common Lisp, ocaml, etc)よりも速いとしたら、それは今回のようにプログラマによるメモリの管理が可能かつ有効な場合ではないかと考えている(つまりポインタ操作の可否)。前回は、URLエンコードにstd::string(C++のライブラリがメモリを管理)を用いたが、その場合は他の言語との差はそれほどなかった(Javasbclでは、文字列のバイト列/ユニコード列変換が必要だということも忘れてはいけない。これは特にデコード時(要バイト列→ユニコード列変換)にコストが大きい)。逆に、高度なメモリ管理(例えばGCのような。スマートポインタはどうだろう?)が必要な領域では、C++を自作のメモリアロケーターなどと組み合わせて使うよりも、良くチューニングされたGCを備えている言語を使った方が速いし、もちろん安全性も高いのではないかと思う。