[gcc最適化]printfがputs呼び出しに置き換えられる場合がある

C言語には、文字表示のためにprintf関数が有ります。

printfは%指定で変数を書式指定付きで埋め込める高機能な関数ですが、高機能な分、処理が遅いというデメリットもあり、書籍によっては “固定の文字を出すだけであればprintf()の替わりにputs()を使用すべし” というHowToが書かれてたりもします。


と今まで思ってましたが…

gccの出力を見ると、時によっては”printfをputs呼び出しに自動で変換してくれる場合がある”ようです。



雰囲気では、printf( “hello\n” );のように、”変数を含まず、かつ、改行で終わる場合”が対象っぽいのですが、どのような場合でprintf呼び出しの最適化が行われるのか、確認してみました。


というわけで、確認用のプログラムです。
面倒なので、確認結果(最適化後の置き換え関数)も先にコメントで書いておきました。

#include<stdio.h>
 
int main()
{
    printf( "test0\n" );                // puts
 
    printf( "test1" );                  // printf
 
    printf( "test2:%s\n", "test2" );    // printf
 
    printf( "%s", "test3\n" );          // puts
 
    printf( "%s\n", "test4" );          // puts
 
    printf( "test5%s\n", "test5" );     // puts
 
    printf( "A" );                      // putchar
 
    printf( "%c", '6' );                // putchar
 
    printf( "%c\n", '7' );              // printf
 
    printf( "%%" );                     // printf
}




説明の順番が前後しますが、上記コードのアセンブラ出力です。

$ gcc -S opt.c
 
 
$ cat opt.s
    .file   "opt.c"
    .section    .rodata
.LC0:
    .string "test0"
.LC1:
    .string "test1"
.LC2:
    .string "test2:%s\n"
.LC3:
    .string "test2"
.LC4:
    .string "test3"
.LC5:
    .string "test4"
.LC6:
    .string "test5%s\n"
.LC7:
    .string "test5"
.LC8:
    .string "%c\n"
.LC9:
    .string "%%"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $16, %esp
    movl    $.LC0, (%esp)
    call    puts
    movl    $.LC1, %eax
    movl    %eax, (%esp)
    call    printf
    movl    $.LC2, %eax
    movl    $.LC3, 4(%esp)
    movl    %eax, (%esp)
    call    printf
    movl    $.LC4, (%esp)
    call    puts
    movl    $.LC5, (%esp)
    call    puts
    movl    $.LC6, %eax
    movl    $.LC7, 4(%esp)
    movl    %eax, (%esp)
    call    printf
    movl    $65, (%esp)
    call    putchar
    movl    $54, (%esp)
    call    putchar
    movl    $.LC8, %eax
    movl    $55, 4(%esp)
    movl    %eax, (%esp)
    call    printf
    movl    $.LC9, %eax
    movl    %eax, (%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1"
    .section    .note.GNU-stack,"",@progbits



この呼び出し関数変更による最適化は、1パターンを除いて、gccに-O0オプション(-O0:最適化しない)をつけた場合でも行われました。



結果を見ると、下記のパターンの時にputs関数に置き換わりました。

    printf( "test0\n" );                // puts
    printf( "%s", "test3\n" );          // puts
    printf( "%s\n", "test4" );          // puts
    printf( "test5%s\n", "test5" );     // puts (-O0だとprintfになる)


仮に%s等の置換部があったとしても、コンパイル時に表示すべき文字列が確定してればputsに置き換えてくれるようです。
4つ目のパターンだけは、なぜか-O0と最適化オプション無しで出力が異なっていました。


以下のパターンでは最適化は行われません。

    printf( "test1" );


これは、出力文字の終端が改行文字ではないパターンは、putsに置き換えできないからです。
putsは引数で与えられた文字列に改行文字を加えて、標準出力に印字します。



また、出力結果が1文字で、かつ末尾に改行が無い場合はputcharに置き換わります。
(例には有りませんが、”\n”だけの出力もputcharになりました)

    printf( "A" );                      // putchar
    printf( "%c", '6' );                // putchar



また、以外な事に”%%\n”や”%%”だと、printfそのままです。

    printf( "%%\n" );                   // printf


これは、putchar()に代替できそうな気もしますが、ダメでした
gccのソースを見たところ以下のコメントがあったので、現状では対応できてないみたいです。
/* We can’t handle anything else with % args or %% … yet. */





なお、このprintf->putchar/putsへの最適化ですが、gccのソースコード的には、c-common.cにあるc_expand_builtin_printf()という関数で行われているようです。

ちょっと長いですが、c_expand_builtin_printf関数のソースをまるっと引用しますので、興味がある方は見てみるのも良いかと思います。
(当然ながら,c_expand_builtin_printf関数に対するのライセンスはGPLです)


細かい事はさておき、コメントを見ながら流れを追うとある程度の雰囲気はつかめるかと思います。

static rtx
c_expand_builtin_printf (arglist, target, tmode, modifier, ignore, unlocked)
     tree arglist;
     rtx target;
     enum machine_mode tmode;
     enum expand_modifier modifier;
     int ignore;
     int unlocked;
{
  tree fn_putchar = unlocked ?
    built_in_decls[BUILT_IN_PUTCHAR_UNLOCKED] : built_in_decls[BUILT_IN_PUTCHAR];
  tree fn_puts = unlocked ?
    built_in_decls[BUILT_IN_PUTS_UNLOCKED] : built_in_decls[BUILT_IN_PUTS];
  tree fn, format_arg, stripped_string;
 
  /* If the return value is used, or the replacement _DECL isn't
     initialized, don't do the transformation.  */
  if (!ignore || !fn_putchar || !fn_puts)
    return 0;
 
  /* Verify the required arguments in the original call.  */
  if (arglist == 0
      || (TREE_CODE (TREE_TYPE (TREE_VALUE (arglist))) != POINTER_TYPE))
    return 0;
 
  /* Check the specifier vs. the parameters.  */
  if (!is_valid_printf_arglist (arglist))
    return 0;
 
  format_arg = TREE_VALUE (arglist);
  stripped_string = format_arg;
  STRIP_NOPS (stripped_string);
  if (stripped_string && TREE_CODE (stripped_string) == ADDR_EXPR)
    stripped_string = TREE_OPERAND (stripped_string, 0);
 
  /* If the format specifier isn't a STRING_CST, punt.  */
  if (TREE_CODE (stripped_string) != STRING_CST)
    return 0;
 
  /* OK!  We can attempt optimization.  */
 
  /* If the format specifier was "%s\n", call __builtin_puts(arg2).  */
  if (strcmp (TREE_STRING_POINTER (stripped_string), "%s\n") == 0)
    {
      arglist = TREE_CHAIN (arglist);
      fn = fn_puts;
    }
  /* If the format specifier was "%c", call __builtin_putchar (arg2).  */
  else if (strcmp (TREE_STRING_POINTER (stripped_string), "%c") == 0)
    {
      arglist = TREE_CHAIN (arglist);
      fn = fn_putchar;
    }
  else
    {
      /* We can't handle anything else with % args or %% ... yet.  */
      if (strchr (TREE_STRING_POINTER (stripped_string), '%'))
  return 0;
 
      /* If the resulting constant string has a length of 1, call
         putchar.  Note, TREE_STRING_LENGTH includes the terminating
         NULL in its count.  */
      if (TREE_STRING_LENGTH (stripped_string) == 2)
        {
    /* Given printf("c"), (where c is any one character,)
             convert "c"[0] to an int and pass that to the replacement
             function.  */
    arglist = build_int_2 (TREE_STRING_POINTER (stripped_string)[0], 0);
    arglist = build_tree_list (NULL_TREE, arglist);
 
    fn = fn_putchar;
        }
      /* If the resulting constant was "string\n", call
         __builtin_puts("string").  Ensure "string" has at least one
         character besides the trailing \n.  Note, TREE_STRING_LENGTH
         includes the terminating NULL in its count.  */
      else if (TREE_STRING_LENGTH (stripped_string) > 2
         && TREE_STRING_POINTER (stripped_string)
         [TREE_STRING_LENGTH (stripped_string) - 2] == '\n')
        {
    /* Create a NULL-terminated string that's one char shorter
       than the original, stripping off the trailing '\n'.  */
    const int newlen = TREE_STRING_LENGTH (stripped_string) - 1;
    char *newstr = (char *) alloca (newlen);
    memcpy (newstr, TREE_STRING_POINTER (stripped_string), newlen - 1);
    newstr[newlen - 1] = 0;
 
    arglist = fix_string_type (build_string (newlen, newstr));
    arglist = build_tree_list (NULL_TREE, arglist);
    fn = fn_puts;
  }
      else
  /* We'd like to arrange to call fputs(string) here, but we
           need stdout and don't have a way to get it ... yet.  */
  return 0;
    }
 
  return expand_expr (build_function_call (fn, arglist),
          (ignore ? const0_rtx : target),
          tmode, modifier);
}


関連記事

コメントを残す

メールアドレスが公開されることはありません。