プログラミング、リファクタリング、そしてすべてにおける究極の疑問: No. 18

同カテゴリーの次の記事

インテル® C++ コンパイラーでサポートされる C++17 の機能

この記事は、インテル® デベロッパー・ゾーンに公開されている「The Ultimate Question of Programming, Refactoring, and Everything」の日本語参考訳です。


18. ある言語の知識は必ずしも別の言語に適用できるとは限らない

Putty プロジェクトから抜粋した以下のコードについて考えてみます。非効率的なコードは、次の PVS-Studio 診断によって検出されます。

V814 Decreased performance. Calls to the ‘strlen’ function have being made multiple times when a condition for the loop’s continuation was calculated. (V814 パフォーマンス低下。ループの継続条件の計算時に ‘strlen’ 関数が複数回呼び出されました。)

static void tell_str(FILE * stream, char *str)
{
  unsigned int i;
  for (i = 0; i < strlen(str); ++i)
    tell_char(stream, str[i]);
}

説明

これは実際にはエラーではありませんが、strlen() 関数がループの各反復で呼び出されているため、長い文字列を扱う場合には非常に非効率的です。

通常、このようなエラーは、Pascal (または Delphi) での開発経験があるプログラマーが記述したコードで見られます。Pascal では、ループの終了条件の評価が一度しか計算されないため、このようなコードが非常に一般的です。

Pascal コードの例を見てみましょう。pstrlen() が一度だけ呼び出されているため、called は一度だけ出力されます。

program test;
var
  i   : integer;
  str : string;

function pstrlen(str : string): integer;
begin
  writeln('called');
  pstrlen := Length(str);
end;

begin
  str := 'a pascal string';
  for i:= 1 to pstrlen(str) do 
    writeln(str[i]);
end.

効率良いコード

static void tell_str(FILE * stream, char *str)
{
  size_t i;
  const size_t len = strlen(str);
  for (i = 0; i < len; ++i)
    tell_char(stream, str[i]);
}

推奨事項

C/C++ では、ループの終了条件が各反復で再計算されます。そのため、非効率な遅い関数の呼び出しを終了判定に含めることは、特にループに入る前にそれを一度計算すれば済む場合、良いアイデアとは言えません。

一部のケースでは、コンパイラーはこのような strlen() を含むコードを最適化できる可能性があります。例えば、ポインターが常に同じ文字列リテラルを参照する場合などです。しかし、それを期待すべきではありません。

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

関連記事