コグノスケ


link 未来から過去へ表示  link 過去から未来へ表示(*)

link もっと前
2021年3月13日 >>> 2021年4月9日
link もっと後

2021年3月13日

GCCを調べる - memmoveのfoldingその1

目次: GCC

GCCのmemmoveに対する最適化とバグっぽい挙動についてのメモです。半年経ったら絶対忘れてわからなくなる自信があるので、書き残しておきます。

検証に使用するのは下記のプログラムです。"AB0123456789abcdefg" が格納された配列sから、同じ配列sに2文字だけずらしてmemmove() します。期待値は先頭2文字が消えた "0123456789abcdefg" です。このプログラムはmemmove() を使う必要があります。memcpy() はコピー元(src)とコピー先(dst)が重複したメモリ領域だと正常に動作しないことがあるからです。

重複したメモリ領域にmemmove() するプログラム

#include <stdio.h>
#include <string.h>

#define PRE "AB"
#define STR "0123456789abcdefg"
#define NOP __asm__ volatile("nop;")

int main(int argc, char *argv[])
{
	volatile char s[] = PRE STR;
	char *p = (char *)s;
	size_t sz_pre = strlen(PRE);
	size_t sz = strlen(p) - sz_pre + 1;

	NOP;
	memmove(p, p + sz_pre, sz);
	NOP; NOP;

	if (strcmp(p, STR) == 0) {
		printf("  OK: %s\n", p);
	} else {
		printf("  NG: %s\n", p);
	}
}
実行結果
$ gcc -O2 -Wall -g test.c

$ ./a.out
  NG: 01234567abcdefg

しかし、実行してみると期待値と一致しません。真ん中の "89" がどこかに消えました。また配列sの宣言からvolatileを削除すると、正常に動作するようになります。一体、何が起きているのでしょうか?

GCCのアグレッシブな最適化

GCCはある条件下でmemmove() を __builtin_memcpy() に置き換えるという最適化を行います。その様子を観察するために、オプション --dump-tree-allを付けて、GIMPLEのファイルを出力します。

GIMPLEを調べる
$ gcc -O2 -Wall -g test.c --dump-tree-all

$ grep -r 'memmove ' ./ | sort

./test.c.004t.original:  memmove ((void *) p, (const void *) (p + (sizetype) sz_pre), sz);
./test.c.005t.gimple:    memmove (p, _3, sz);
./test.c.007t.omplower:    memmove (p, _3, sz);
./test.c.008t.lower:  memmove (p, _3, sz);
./test.c.011t.eh:  memmove (p, _3, sz);
./test.c.013t.cfg:  memmove (p, _3, sz);
./test.c.015t.ompexp:  memmove (p, _3, sz);
./test.c.020t.fixup_cfg1:  memmove (p, _3, sz);
./test.c.021t.ssa:  memmove (p_8, _3, sz_10);
./test.c.023t.nothrow:  memmove (p_8, _3, sz_10);
./test.c.025t.fixup_cfg2:  memmove (p_8, _3, sz_10);
./test.c.026t.local-fnsummary1:  memmove (p_8, _3, sz_10);
./test.c.027t.einline:  memmove (p_8, _3, sz_10);
./test.c.028t.early_optimizations:  memmove (p_8, _3, sz_10);
./test.c.029t.objsz1:  memmove (p_8, _3, sz_10);

ファイルをmemmoveで検索すると、029t.objsz1を最後に出現しなくなっています。029t.objsz1の次である030t.cpp1を見て、memmoveがどうなったのか確かめます。

030t.ccp1

// test.c.030t.ccp1

;; Function main (main, funcdef_no=11, decl_uid=2555, cgraph_uid=12, symbol_order=11)

main (int argc, char * * argv)
{

...

  __asm__ __volatile__("nop;");
  # DEBUG BEGIN_STMT
  __builtin_memcpy (&s, &MEM <volatile char[20]> [(void *)&s + 2B], sz_10);
  # DEBUG BEGIN_STMT
  __asm__ __volatile__("nop;");
  # DEBUG BEGIN_STMT
  __asm__ __volatile__("nop;");

...

検証用プログラムではmemmoveと書いたはずの場所が、__builtin_memcpy() に変わっています。この __builtin_memcpy() は最終的にアセンブラに展開されます。あまり詳しく追っていませんが、末尾の余りバイトをコピーしてから、先頭から8バイトずつコピーするループが実行されるようで、srcとdestが重なっていると、正常に動作しません。

逆アセンブル

$ objdump -drS a.out

...

        NOP;
    1084:       90                      nop
        size_t sz = strlen(p) - sz_pre + 1;
    1085:       48 8d 50 ff             lea    -0x1(%rax),%rdx
        memmove(p, p + sz_pre, sz);
    1089:       48 8d 4c 24 02          lea    0x2(%rsp),%rcx
    108e:       48 83 fa 08             cmp    $0x8,%rdx
    1092:       73 52                   jae    10e6 <main+0x86>    ★コピーするコードの本体
    1094:       f6 c2 04                test   $0x4,%dl
    1097:       0f 85 85 00 00 00       jne    1122 <main+0xc2>
    109d:       48 85 d2                test   %rdx,%rdx
    10a0:       74 0f                   je     10b1 <main+0x51>
    10a2:       0f b6 01                movzbl (%rcx),%eax
    10a5:       88 45 00                mov    %al,0x0(%rbp)
    10a8:       f6 c2 02                test   $0x2,%dl
    10ab:       0f 85 80 00 00 00       jne    1131 <main+0xd1>
        NOP; NOP;
    10b1:       90                      nop
    10b2:       90                      nop

...

        memmove(p, p + sz_pre, sz);
    10e6:       48 8b 74 14 fa          mov    -0x6(%rsp,%rdx,1),%rsi
    10eb:       48 83 e8 02             sub    $0x2,%rax
    10ef:       48 89 74 14 f8          mov    %rsi,-0x8(%rsp,%rdx,1)
    10f4:       48 83 f8 08             cmp    $0x8,%rax
    10f8:       72 b7                   jb     10b1 <main+0x51>
    10fa:       48 83 e0 f8             and    $0xfffffffffffffff8,%rax
    10fe:       31 d2                   xor    %edx,%edx
    1100:       48 8b 34 11             mov    (%rcx,%rdx,1),%rsi      ★この辺りがコピーのためのループ
    1104:       48 89 74 15 00          mov    %rsi,0x0(%rbp,%rdx,1)
    1109:       48 83 c2 08             add    $0x8,%rdx
    110d:       48 39 c2                cmp    %rax,%rdx
    1110:       72 ee                   jb     1100 <main+0xa0>
    1112:       eb 9d                   jmp    10b1 <main+0x51>
                printf("  OK: %s\n", p);
    1114:       48 8d 3d fb 0e 00 00    lea    0xefb(%rip),%rdi        # 2016 <_IO_stdin_used+0x16>
    111b:       e8 20 ff ff ff          callq  1040 <printf@plt>
    1120:       eb bc                   jmp    10de <main+0x7e>
        memmove(p, p + sz_pre, sz);
    1122:       8b 01                   mov    (%rcx),%eax
    1124:       89 45 00                mov    %eax,0x0(%rbp)
    1127:       8b 44 11 fc             mov    -0x4(%rcx,%rdx,1),%eax
    112b:       89 44 15 fc             mov    %eax,-0x4(%rbp,%rdx,1)
    112f:       eb 80                   jmp    10b1 <main+0x51>
    1131:       0f b7 44 11 fe          movzwl -0x2(%rcx,%rdx,1),%eax
    1136:       66 89 44 15 fe          mov    %ax,-0x2(%rbp,%rdx,1)
    113b:       e9 71 ff ff ff          jmpq   10b1 <main+0x51>

長くなってきたので、また次回。

編集者:すずき(2023/09/24 11:53)

コメント一覧

  • emkさん(2024/03/05 12:44)
    キャストでvolatileを外してアクセスしている時点で未定義動作なので、その後の調査には何の意味もありませんね。GCCではなくこのプログラムがバグっているだけです。
  • すずきさん(2024/03/05 15:13)
    あー、このプログラムがまずいんですね。ご指摘ありがとうございます。
open/close この記事にコメントする



2021年3月14日

GCCを調べる - memmoveのfoldingその2

目次: GCC

前回はmemmove() が030t.ccp1という最適化パスにて __builtin_memcpy() に置き換えられ、誤動作してしまうところまで見ました。今回はどこで置き換えられているのか、追いかけます。

入れ替えを行っている場所はgimple_fold_builtin_memory_op() という関数です。コールスタックは下記のようになります。今回はGCC 11というかHEADを使います。関数名は若干違いますがGCC 8くらいでも大筋は同じです。

memmove置き換え箇所までのコールスタック
do_ssa_ccp()  @tree-ssa-ccp.c
  ccp_finalize()
    substitute_and_fold_engine::substitute_and_fold()  @tree-ssa-propagate.c
      substitute_and_fold_dom_walker walker (CDI_DOMINATORS, this);
      walker.walk (ENTRY_BLOCK_PTR_FOR_FN (cfun));
        dom_walker::walk()  @domwalk.c
          substitute_and_fold_dom_walker::before_dom_children()  @tree-ssa-propagate.c
            fold_stmt()  @gimple-fold.c
              fold_stmt_1()  @gimple-fold.c
                gimple_fold_call()
                  gimple_fold_builtin()
                    gimple_fold_builtin_memory_op()

置き換えている箇所はgimple_fold_builtin_memory_op() 内に3箇所ありますが、今回のケースに該当するのは下記の部分です。

memmove置き換え箇所

// gcc/gcc/gimple-fold.c

static bool
gimple_fold_builtin_memory_op (gimple_stmt_iterator *gsi,
			       tree dest, tree src, enum built_in_function code)
{
  gimple *stmt = gsi_stmt (*gsi);
  tree lhs = gimple_call_lhs (stmt);
  tree len = gimple_call_arg (stmt, 2);
  location_t loc = gimple_location (stmt);

...

      if (code == BUILT_IN_MEMMOVE)
	{

...

	  /* If *src and *dest can't overlap, optimize into memcpy as well.  */
	  if (TREE_CODE (src) == ADDR_EXPR
	      && TREE_CODE (dest) == ADDR_EXPR)
	    {

              //...ここでチェックしている...

	      fn = builtin_decl_implicit (BUILT_IN_MEMCPY);
	      if (!fn)
		return false;
	      gimple_call_set_fndecl (stmt, fn);    //★★ここでmemmove -> memcpyに置き換わる
	      gimple_call_set_arg (stmt, 0, dest);
	      gimple_call_set_arg (stmt, 1, src);
	      fold_stmt (gsi);
	      return true;
	    }

...

/*
 * 補足: gimple * の中身が見たいときは、下記のようにすると表示できます。
 *
 *   print_gimple_stmt (stdout, stmt, 0);       //HEAD
 *   print_gimple_stmt (stdout, stmt, 0, 0);    //GCC 8あたり
 */

検証プログラムはmemmove() の引数にポインタを渡しているので、TREE_CODE (src) == ADDR_EXPR && TREE_CODE (dest) == ADDR_EXPRの条件が成立します。

置き換えの前にはいくつかチェックがあって、memmove() をmemcpy() に置き換えるべきではないと判断したらreturn false; し、置き換え箇所に到達しない仕組みになっています。

memmove置き換え前のチェック箇所

// gcc/gcc/gimple-fold.c

	  /* If *src and *dest can't overlap, optimize into memcpy as well.  */
	  if (TREE_CODE (src) == ADDR_EXPR
	      && TREE_CODE (dest) == ADDR_EXPR)
	    {
	      tree src_base, dest_base, fn;
	      poly_int64 src_offset = 0, dest_offset = 0;
	      poly_uint64 maxsize;

              //★★src->expr.operands[0] を返す、srcvarはMEM_REFタイプ
	      srcvar = TREE_OPERAND (src, 0);
              //★★src_baseはVAR_DECLタイプ
              //★★src_offsetは2(検証用プログラムでmemmoveのsrcにs + 2を渡しているから)
	      src_base = get_addr_base_and_unit_offset (srcvar, &src_offset);
	      if (src_base == NULL)
		src_base = srcvar;

              //★★dest->expr.operands[0] を返す、destvarはMEM_REFタイプ
	      destvar = TREE_OPERAND (dest, 0);
              //★★dest_baseはVAR_DECLタイプ
              //★★dest_offsetは0(検証用プログラムでdestにsを渡しているから)
	      dest_base = get_addr_base_and_unit_offset (destvar,
							 &dest_offset);
	      if (dest_base == NULL)
		dest_base = destvar;
	      if (!poly_int_tree_p (len, &maxsize))
		maxsize = -1;
	      if (SSA_VAR_P (src_base)
		  && SSA_VAR_P (dest_base))    //★★この条件が成立する
		{
                  //★★volatileがあるとoperand_equal_p() がfalseになり
                  //★★memmoveがmemcpyに置き換えられてしまう
		  if (operand_equal_p (src_base, dest_base, 0)
		      && ranges_maybe_overlap_p (src_offset, maxsize,
						 dest_offset, maxsize))
		    return false;    //★★チェック1箇所目
		}
	      else if (TREE_CODE (src_base) == MEM_REF
		       && TREE_CODE (dest_base) == MEM_REF)
		{
		  if (! operand_equal_p (TREE_OPERAND (src_base, 0),
					 TREE_OPERAND (dest_base, 0), 0))
		    return false;
		  poly_offset_int full_src_offset
		    = mem_ref_offset (src_base) + src_offset;
		  poly_offset_int full_dest_offset
		    = mem_ref_offset (dest_base) + dest_offset;
		  if (ranges_maybe_overlap_p (full_src_offset, maxsize,
					      full_dest_offset, maxsize))
		    return false;    //★★チェック2箇所目
		}
	      else
		return false;    //★★チェック3箇所目

	      fn = builtin_decl_implicit (BUILT_IN_MEMCPY);
	      if (!fn)
		return false;
	      gimple_call_set_fndecl (stmt, fn);    //★★ここでmemmove -> memcpyに置き換わる
	      gimple_call_set_arg (stmt, 0, dest);
	      gimple_call_set_arg (stmt, 1, src);
	      fold_stmt (gsi);
	      return true;
...


// gcc/gcc/tree-dfa.c

/* Returns the base object and a constant BITS_PER_UNIT offset in *POFFSET that
   denotes the starting address of the memory access EXP.
   Returns NULL_TREE if the offset is not constant or any component
   is not BITS_PER_UNIT-aligned.  */

tree
get_addr_base_and_unit_offset (tree exp, poly_int64_pod *poffset)
{
  return get_addr_base_and_unit_offset_1 (exp, poffset, NULL);
}


/* Returns the base object and a constant BITS_PER_UNIT offset in *POFFSET that
   denotes the starting address of the memory access EXP.
   Returns NULL_TREE if the offset is not constant or any component
   is not BITS_PER_UNIT-aligned.
   VALUEIZE if non-NULL is used to valueize SSA names.  It should return
   its argument or a constant if the argument is known to be constant.  */

tree
get_addr_base_and_unit_offset_1 (tree exp, poly_int64_pod *poffset,
				 tree (*valueize) (tree))
{
  poly_int64 byte_offset = 0;

  /* Compute cumulative byte-offset for nested component-refs and array-refs,
     and find the ultimate containing object.  */
  while (1)
    {
      switch (TREE_CODE (exp))
	{

...

	case MEM_REF:
	  {
	    tree base = TREE_OPERAND (exp, 0);
	    if (valueize
		&& TREE_CODE (base) == SSA_NAME)
	      base = (*valueize) (base);

	    /* Hand back the decl for MEM[&decl, off].  */
	    if (TREE_CODE (base) == ADDR_EXPR)
	      {
		if (!integer_zerop (TREE_OPERAND (exp, 1)))
		  {
		    poly_offset_int off = mem_ref_offset (exp);
		    byte_offset += off.force_shwi ();
		  }
		exp = TREE_OPERAND (base, 0);
	      }
	    goto done;
	  }


// gcc/gcc/tree.h

#define SSA_VAR_P(DECL)							\
	(TREE_CODE (DECL) == VAR_DECL					\
	 || TREE_CODE (DECL) == PARM_DECL				\
	 || TREE_CODE (DECL) == RESULT_DECL				\
	 || TREE_CODE (DECL) == SSA_NAME)

領域が重複している場合は、1箇所目のチェックに引っかかります。不思議なことに、配列sの宣言からvolatileを外して動かすと、1箇所目のチェックに引っかかりますが、volatileを付けるとチェックに引っかからなくなります。

検証用のコード(部分、再掲)

int main(int argc, char *argv[])
{
	volatile char s[] = PRE STR;    //★★volatileを外すと __builtin_memcpy() への置き換えが発生しなくなる
	char *p = (char *)s;
	size_t sz_pre = strlen(PRE);
	size_t sz = strlen(p) - sz_pre + 1;

本来はvolatileがあろうがなかろうが1箇所目のチェックに引っかからないとおかしいですが、volatileがあるときは、なぜか1箇所目のチェックを通過してしまいます。

長くなってきたので、続きはまた次回。

編集者:すずき(2023/09/24 11:53)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年3月15日

GCCを調べる - memmoveのfoldingその3

目次: GCC

前回はmemmove() をmemcpy() に置き換える条件をチェックしているコードを紹介しました。より詳細に見ていきたいと思います。ここまで細かい話になると、もはや自分以外の誰得なのか全くわかりませんけど、そんなことを気にしてはいけません。

volatileの有無で何が変わるか?

前回紹介したチェック箇所のうちvolatileの有無で動きが変わるのは、下記のoperand_equal_p() です。

memmove置き換え前のチェック箇所

// gcc/gcc/gimple-fold.c

	      if (SSA_VAR_P (src_base)
		  && SSA_VAR_P (dest_base))    //★★この条件が成立する
		{
                  //★★volatileがあるとoperand_equal_p() がfalseを返し、
                  //★★memmoveがmemcpyに置き換えられてしまう
		  if (operand_equal_p (src_base, dest_base, 0)
		      && ranges_maybe_overlap_p (src_offset, maxsize,
						 dest_offset, maxsize))
		    return false;    //★★チェック1箇所目
		}


// gcc/gcc/fold-const.c

/* Return nonzero if two operands (typically of the same tree node)
   are necessarily equal. FLAGS modifies behavior as follows:

   If OEP_ONLY_CONST is set, only return nonzero for constants.
   This function tests whether the operands are indistinguishable;
   it does not test whether they are equal using C's == operation.
   The distinction is important for IEEE floating point, because
   (1) -0.0 and 0.0 are distinguishable, but -0.0==0.0, and
   (2) two NaNs may be indistinguishable, but NaN!=NaN.

   If OEP_ONLY_CONST is unset, a VAR_DECL is considered equal to itself
   even though it may hold multiple values during a function.
   This is because a GCC tree node guarantees that nothing else is
   executed between the evaluation of its "operands" (which may often
   be evaluated in arbitrary order).  Hence if the operands themselves
   don't side-effect, the VAR_DECLs, PARM_DECLs etc... must hold the
   same value in each operand/subexpression.  Hence leaving OEP_ONLY_CONST
   unset means assuming isochronic (or instantaneous) tree equivalence.
   Unless comparing arbitrary expression trees, such as from different
   statements, this flag can usually be left unset.

   If OEP_PURE_SAME is set, then pure functions with identical arguments
   are considered the same.  It is used when the caller has other ways
   to ensure that global memory is unchanged in between.

   If OEP_ADDRESS_OF is set, we are actually comparing addresses of objects,
   not values of expressions.

   If OEP_LEXICOGRAPHIC is set, then also handle expressions with side-effects
   such as MODIFY_EXPR, RETURN_EXPR, as well as STATEMENT_LISTs.

   If OEP_BITWISE is set, then require the values to be bitwise identical
   rather than simply numerically equal.  Do not take advantage of things
   like math-related flags or undefined behavior; only return true for
   values that are provably bitwise identical in all circumstances.

   Unless OEP_MATCH_SIDE_EFFECTS is set, the function returns false on
   any operand with side effect.  This is unnecesarily conservative in the
   case we know that arg0 and arg1 are in disjoint code paths (such as in
   ?: operator).  In addition OEP_MATCH_SIDE_EFFECTS is used when comparing
   addresses with TREE_CONSTANT flag set so we know that &var == &var
   even if var is volatile.  */

bool
operand_compare::operand_equal_p (const_tree arg0, const_tree arg1,
				  unsigned int flags)
{
  bool r;
  if (verify_hash_value (arg0, arg1, flags, &r))
    return r;

...

  /* If ARG0 and ARG1 are the same SAVE_EXPR, they are necessarily equal.
     We don't care about side effects in that case because the SAVE_EXPR
     takes care of that for us. In all other cases, two expressions are
     equal if they have no side effects.  If we have two identical
     expressions with side effects that should be treated the same due
     to the only side effects being identical SAVE_EXPR's, that will
     be detected in the recursive calls below.
     If we are taking an invariant address of two identical objects
     they are necessarily equal as well.  */
  if (arg0 == arg1 && ! (flags & OEP_ONLY_CONST)
      && (TREE_CODE (arg0) == SAVE_EXPR
	  || (flags & OEP_MATCH_SIDE_EFFECTS)
	  || (! TREE_SIDE_EFFECTS (arg0) && ! TREE_SIDE_EFFECTS (arg1))))
    return true;  //★★volatileがないときは、このチェックに引っかかるが、volatileがあると引っかからない

...

変化する箇所を見つけましたので、条件を全部バラしてvolatileの有無でどの条件が変化するか調べます。GCCはこういう訳のわからないif文を連発してくるため、非常に解析が大変です……。

volatileの有無で変化する条件を調べる

// ★★下記のように条件を全て展開して差分を調べる

int a, b, c, d, e, f, g;

a = arg0 == arg1;
b = ! (flags & OEP_ONLY_CONST);
c = TREE_CODE (arg0) == SAVE_EXPR;
d = flags & OEP_MATCH_SIDE_EFFECTS;
e = ! TREE_SIDE_EFFECTS (arg0);    //★★この条件が変わる
f = ! TREE_SIDE_EFFECTS (arg1);    //★★この条件が変わる
g = ppa && ppb && (ppc || ppd || (ppe && ppf));

/*
 * volatileなし
 *   (a, b, c, d, e, f, g)
 *   (1, 1, 0, 0, 1, 1, 1)
 * volatileあり
 *   (a, b, c, d, e, f, g)
 *   (1, 1, 0, 0, 0, 0, 0)
 *
 * volatileありのときはside_effects_flagが1になる。
 */


// gcc/gcc/tree.h

/* In any expression, decl, or constant, nonzero means it has side effects or
   reevaluation of the whole expression could produce a different value.
   This is set if any subexpression is a function call, a side effect or a
   reference to a volatile variable.  In a ..._DECL, this is set only if the
   declaration said `volatile'.  This will never be set for a constant.  */
#define TREE_SIDE_EFFECTS(NODE) \
  (NON_TYPE_CHECK (NODE)->base.side_effects_flag)


//★★参考: side_effects_flagを設定する場所
// gcc/gcc/c-family/c-common.c

/* Apply the TYPE_QUALS to the new DECL.  */

void
c_apply_type_quals_to_decl (int type_quals, tree decl)
{
  tree type = TREE_TYPE (decl);

  if (type == error_mark_node)
    return;

  if ((type_quals & TYPE_QUAL_CONST)
      || (type && TREE_CODE (type) == REFERENCE_TYPE))
    /* We used to check TYPE_NEEDS_CONSTRUCTING here, but now a constexpr
       constructor can produce constant init, so rely on cp_finish_decl to
       clear TREE_READONLY if the variable has non-constant init.  */
    TREE_READONLY (decl) = 1;
  if (type_quals & TYPE_QUAL_VOLATILE)
    {
      TREE_SIDE_EFFECTS (decl) = 1;    //★★ここで設定する
      TREE_THIS_VOLATILE (decl) = 1;
    }
  if (type_quals & TYPE_QUAL_RESTRICT)
    {
      while (type && TREE_CODE (type) == ARRAY_TYPE)
	/* Allow 'restrict' on arrays of pointers.
	   FIXME currently we just ignore it.  */
	type = TREE_TYPE (type);
      if (!type
	  || !POINTER_TYPE_P (type)
	  || !C_TYPE_OBJECT_OR_INCOMPLETE_P (TREE_TYPE (type)))
	error ("invalid use of %<restrict%>");
    }
}

もしsrcやdestにvolatile修飾子が付いていると、side_effects_flagがセットされます。このフラグがセットされていると、GCCはアドレスをチェックすることなく、問答無用でsrcとdestは「等しくない」と判断します。その結果srcとdestは重なっていないことになって、memmove() をmemcpy() に置き換える最適化が働きます。

正直な感想としてはGCCバグってるだろ……と思いますが、volatile変数はいつ書き換わってもおかしくないので、memmove() をmemcpy() に置き換えて、動作がおかしくなっても規格違反にはならない?うーん、良くわかりませんね。

ちなみにClang/LLVMはこのような最適化は行いません。memmove() はmemmove() の呼び出しのままです。

x86_64環境の事情

さらに厄介なことにx86_64のglibcはmemmove() を呼ぶべき場面でmemcpy() を呼んでも、正常に動いてしまいます。検証用のプログラムを下記のように変えます。

重複したメモリ領域にmemcpy() するプログラム

#include <stdio.h>
#include <string.h>

#define PRE "AB"
#define STR "0123456789abcdefg"
#define NOP __asm__ volatile("nop;")

int main(int argc, char *argv[])
{
	volatile char s[] = PRE STR;
	char *p = (char *)s;
	size_t sz_pre = strlen(PRE);
	size_t sz = strlen(p) - sz_pre + 1;

	NOP;
	memcpy(p, p + sz_pre, sz);
	NOP; NOP;

	if (strcmp(p, STR) == 0) {
		printf("  OK: %s\n", p);
	} else {
		printf("  NG: %s\n", p);
	}
}

オプション -fno-builtinを指定すると、GCCがmemcpy() をアセンブラへ展開しようとする最適化を抑制することができます。

重複したメモリ領域にmemcpy() するプログラムの実行結果
$ gcc -O2 -Wall -g test.c -fno-builtin

$ objdump -drS a.out

...

        NOP;
    10b7:       90                      nop
        size_t sz = strlen(p) - sz_pre + 1;
    10b8:       48 83 c2 01             add    $0x1,%rdx
        memcpy(p, p + sz_pre, sz);
    10bc:       48 8d 74 1d 00          lea    0x0(%rbp,%rbx,1),%rsi
    10c1:       48 89 ef                mov    %rbp,%rdi
        size_t sz = strlen(p) - sz_pre + 1;
    10c4:       48 29 da                sub    %rbx,%rdx
        memcpy(p, p + sz_pre, sz);
    10c7:       e8 94 ff ff ff          callq  1060 <memcpy@plt>    ;★★memcpyを呼んでいる
        NOP; NOP;
    10cc:       90                      nop
    10cd:       90                      nop


$ ./a.out

  OK: 0123456789abcdefg

この動作でもC言語仕様に違反するわけではありません。しかしGCCが間違ってmemmove() をmemcpy() に置き換えるようなバグがあったときに、意図せず隠蔽してしまいます。ありがたいような、ありがたくないような実装ですね。

編集者:すずき(2023/09/24 11:53)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年3月18日

こっそり下がっている椅子

人によると思うんですけど、私は机に対して椅子の高さが低すぎると、肘で体重を支えるようになり異常に肩が痛くなります。肩が痛いのは嫌なので椅子の高さを調節していたはずなのに、なぜか最近また肩が痛くなってきました。

これはおかしいと思って椅子の高さを確認すると、いつの間にか下がっています。高さ調整機能がヘタっているのか?時間とともにジワジワ下がっているようです。

なんてことするんだ、やめてー。

メモ: 技術系?の話はFacebookから転記しておくことにした。

編集者:すずき(2022/04/06 18:53)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする




2021年3月25日

3階に住んで20年

実家を出て20年が経ちました。振り返ると、9割「3階に住んで」います。自分の意志で借りた部屋に限れば、全て3階です。住所の遍歴はこんな感じ。

  • 大学の寮: 113号室(4階建て)1年
  • つくば: 308号室(3階建て)6年
  • 会社の寮: 2階?(10階建て)1年
  • 大阪: 301号室(4階建て)10年
  • 東京: 306号室(5階建て)2年

こう書くと、3階に恨みでもあるか、憑りついている地縛霊みたいですが、私は人間です。さておき真面目な話、1階の部屋と3階の部屋(2〜5階もほぼ同条件)を比べ下記の点が気に入っています。

  • 明るい
  • 外から部屋が見えない
  • 最悪、階段で生活できる(災害時など)

こちらから不動産屋に「3階がいい」と1度も言ったことはないので、1度くらい4階や5階と巡り合っても不思議はなかったはずですが、結局ずっと3階でした。謎の縁ですね。

メモ: 技術系の話はFacebookから転記しておくことにした。

編集者:すずき(2021/03/27 11:16)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年3月26日

たかいところこわい

3階で思い出したんですが、私は高所恐怖症らしく、背丈の2〜3倍くらいの高さから下を見ると、動悸がして手から変な汗が出ます。

崖、高い吊り橋など、誰でも怖いところは当然怖いんですけど、他の人が怖くないのに、私だけ怖がっている(=理解してもらえない)恐怖スポットとして、

  • 歩道橋
  • 地面が見える階段(踏み板が網やガラスなどのタイプ)
  • 向こうが見える階段(蹴り込み板がないタイプ、例えばこれの「デザイン階段」みたいなやつ)
  • 2階以上で、足下までガラスの窓
  • 2階以上で、向こうが見える柵(※)

(※)自分の背の半分以下の柵、高さがあっても頭が通るくらいの隙間が空いてる柵は怖いです。


階段の板の名前


私が怖いと感じるタイプの階段(パナソニックのサイトから引用)

何が怖いの?と聞かれますが、説明が難しいです。強いて言えば「一歩踏み出したら隙間に吸い込まれそうな恐怖感」でしょうか。隙間から落ちることはないと理解していても、それでも怖いから不思議です。

観光地の「景観スポット」はたいてい恐怖スポットです。さらに良くないことに、周りの人は私が冗談を言ってるように聞こえるようで、ふざけて段差側に押されたりします。本当に恐怖です。勘弁して……。

いろいろある恐怖症

Facebookではいくつかコメントいただいて、興味深かったです。高いところが怖いというのは割と普遍的ですが、怖さの感じるポイントや、感じ方は人それぞれです。

閉所恐怖症なんてのも教えてもらいました。そういうのもあるのか。この年になるとどこかに閉じ込められるという経験をすることはほぼないので、自分が閉所恐怖症なのかどうかすらわからないですね。

メモ: 技術系の話はFacebookから転記しておくことにした。階段の板の名前の図を追加。

編集者:すずき(2021/03/27 12:10)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年3月29日

GCCを調べる - デバッグ環境再び

目次: GCC

去年あたりにGCCのデバッグ環境について書きました(2019年5月17日の日記参照)。GDBでGCCの動作を調べる際に、最適化が効いていると色々デバッグ時に不都合が生じます。例えば、

  • ブレークポイントが設定できない箇所が生じる(インライン関数、最適化で消える部分など)
  • 関数コールのバックトレースが不完全、動作と合わない(インライン関数、末尾最適化でretが消える場合など)
  • 引数、ローカル変数の値がoptimized outされて読めない

通常のアプリケーションだと気になりませんが、相手は魔界GCCです。勝手にブレークポイントがずれてもらってはたまったものではないです。こういうときはGCCのビルドオプションを変えて、最適化を全てOFFにしてしまうと見やすいです。

環境はDebian Testingです。GCCは8.3を使っています。

最適化をOFFにしてGCCをビルド
$ mkdir build
$ cd build

$ ../configure \
  --prefix=`pwd`/_install \
  --enable-languages=c,c++ \
  --disable-libsanitizer \
  --disable-bootstrap \
  CFLAGS="-O0 -g -fno-inline" \
  CXXFLAGS="-O0 -g -fno-inline"

$ make -j8
$ make install

私はCとC++ だけ使えれば良いので、--enable-languages=c,c++ を指定して、ついでにビルド時間短縮しています。C以外の言語(Fortranなど)に用事がある場合は、適宜足してください。Debian TestingでGCC 8.3をビルドするとなんでかlibsanitizerがビルドエラーになった(深追いしてません)ので、--disable-libsanitizerを付けて回避しました。環境によっては要らないかも?

また、ブートストラップモードだと、ビルドに時間がかかりすぎるので --disable-bootstrapで無効にしています。ビルドオプションは "-O0 -g -fno-inline" 最適化なし、デバッグ情報あり、インライン展開なし、です。

インストール先はどこでも良いですが、インストールするディレクトリは、build下の _installディレクトリにしています。buildディレクトリと一緒に消せて、間違って古いバイナリを使う心配がほぼないため、最近お気に入りのインストール先です。

最適化ありの場合

オプション -O2 -gでビルドしたバイナリを使って、GDBでインライン関数にブレークポイントを設定すると、こんなふうになります。

ブレークポイント設定先の関数

/* Set the implicit flag for a builtin function.  */

static inline void
set_builtin_decl_implicit_p (enum built_in_function fncode, bool implicit_p)
{
  size_t uns_fncode = (size_t)fncode;

  gcc_checking_assert (BUILTIN_VALID_P (fncode)
		       && builtin_info[uns_fncode].decl != NULL_TREE);

  builtin_info[uns_fncode].implicit_p = implicit_p;    //★ここにブレークポイントを設定★
}
static inline関数でブレークポイント(最適化あり)
$ gdb /path/to/gcc/build/_install/libexec/gcc/x86_64-pc-linux-gnu/8.3.0/cc1

(gdb) b tree.h:5245
Breakpoint 1 at 0x7cc7c3: tree.h:5245. (2 locations)

(gdb) r -quiet -imultiarch x86_64-linux-gnu a.c -dumpbase a.c -mtune=generic \
  -march=x86-64 -auxbase a -g -O2 -Wall -std=c99 -o zzzzzzzz.s

Breakpoint 1, set_builtin_decl_implicit_p (implicit_p=true, fncode=12304)
    at ../../gcc/tree.h:5245
5245      builtin_info[uns_fncode].implicit_p = implicit_p;

(gdb) p uns_fncode

$1 = <optimized out>

一応ブレークはしますが、表示がおかしいです。引数の順序が逆ですし、fncodeの値もおかしい(16のはず)です。ローカル変数は最適化によって消されてprint不可能です。

バックトレース(最適化あり)
#0  set_builtin_decl_implicit_p (implicit_p=true, fncode=12304)
    at ../../gcc/tree.h:5245
#1  gimplify_addr_expr (expr_p=expr_p@entry=0x7ffff76682e0, pre_p=pre_p@entry=0x7fffffffd600, post_p=post_p@entry=0x7fffffffd190)
    at ../../gcc/gimplify.c:6051
#2  0x000000000084301d in gimplify_expr (expr_p=0x7ffff76682e0, pre_p=<optimized out>, post_p=<optimized out>, gimple_test_f=<optimized out>, fallback=<optimized out>)
    at ../../gcc/gimplify.c:11581
#3  0x0000000000846a55 in gimplify_call_expr (expr_p=0x7ffff77eaee0, pre_p=0x7fffffffd600, want_value=<optimized out>)
    at ../../gcc/gimplify.c:3308
#4  0x00000000008436d6 in gimplify_expr (expr_p=0x7ffff77eaee0, pre_p=<optimized out>, post_p=<optimized out>, gimple_test_f=<optimized out>, fallback=<optimized out>)
    at ../../gcc/gimplify.c:11506
#5  0x0000000000843c0e in gimplify_stmt (seq_p=<optimized out>, stmt_p=<optimized out>)
    at ../../gcc/gimplify.c:6690
#6  gimplify_statement_list (pre_p=<optimized out>, expr_p=0x7ffff743ddd0)
    at ../../gcc/gimplify.c:1764
#7  gimplify_expr (expr_p=0x7ffff743ddd0, pre_p=<optimized out>, post_p=<optimized out>, gimple_test_f=<optimized out>, fallback=<optimized out>)
    at ../../gcc/gimplify.c:11963
#8  0x0000000000848f42 in gimplify_stmt (seq_p=0x7fffffffd600, stmt_p=0x7ffff743ddd0)
    at ../../gcc/gimplify.c:6690
#9  gimplify_bind_expr (expr_p=expr_p@entry=0x7ffff744b1c0, pre_p=pre_p@entry=0x7fffffffd7e8)
    at ../../gcc/gimplify.c:1331
#10 0x000000000084344b in gimplify_expr (expr_p=0x7ffff744b1c0, pre_p=<optimized out>, post_p=<optimized out>, gimple_test_f=<optimized out>, fallback=<optimized out>)
    at ../../gcc/gimplify.c:11735
#11 0x0000000000847508 in gimplify_stmt (seq_p=0x7fffffffd7e8, stmt_p=0x7ffff744b1c0)
    at ../../gcc/gimplify.c:6690
#12 gimplify_body (fndecl=0x7ffff744b100, do_parms=<optimized out>)
    at ../../gcc/gimplify.c:12735
#13 0x00000000008478e6 in gimplify_function_tree (fndecl=fndecl@entry=0x7ffff744b100)
    at ../../gcc/gimplify.c:12900
#14 0x00000000006f6ab0 in cgraph_node::analyze (this=0x7ffff74472e0)
    at ../../gcc/cgraphunit.c:670
#15 0x00000000006f8e68 in analyze_functions (first_time=<optimized out>)
    at ../../gcc/cgraphunit.c:1131
#16 0x00000000006f99c3 in symbol_table::finalize_compilation_unit (this=0x7ffff7658100)
    at ../../gcc/cgraphunit.c:2691
#17 0x0000000000a689fb in compile_file ()
    at ../../gcc/toplev.c:480
#18 0x00000000005bbe3d in do_compile ()
    at ../../gcc/toplev.c:2132
#19 toplev::main (this=this@entry=0x7fffffffda9e, argc=<optimized out>, argc@entry=17, argv=<optimized out>, argv@entry=0x7fffffffdba8)
    at ../../gcc/toplev.c:2267
#20 0x00000000005be0cf in main (argc=17, argv=0x7fffffffdba8)
    at ../../gcc/main.c:39

このケースだとバックトレースに抜けはなさそうですが、表示される引数にoptimized outが多く、何が渡されたのかわかりません。

最適化なしの場合

ビルドオプション -O0 -g -fno-inlineでビルドして、GDBでインライン関数にブレークを設定します。

static inline関数にブレークポイント(最適化なし)
$ gdb /path/to/gcc/build/_install/libexec/gcc/x86_64-pc-linux-gnu/8.3.0/cc1

(gdb) b tree.h:5245
Breakpoint 1 at 0x7cc7c3: tree.h:5245. (2 locations)

(gdb) r -quiet -imultiarch x86_64-linux-gnu a.c -dumpbase a.c -mtune=generic \
  -march=x86-64 -auxbase a -g -O2 -Wall -std=c99 -o zzzzzzzz.s

Breakpoint 1, set_builtin_decl_implicit_p (fncode=BUILT_IN_ATAN2F,
    implicit_p=true) at ../../gcc/tree.h:5245
5245      builtin_info[uns_fncode].implicit_p = implicit_p;

(gdb) p uns_fncode

$1 = 16

引数のenumも名前で出ていますし、ローカル変数も表示できます。

バックトレース(最適化なし)
#0  set_builtin_decl_implicit_p (fncode=BUILT_IN_ATAN2F, implicit_p=true)
    at ../../gcc/tree.h:5245
#1  0x0000000000b55843 in gimplify_addr_expr (expr_p=0x7ffff76682e0, pre_p=0x7fffffffd3f0, post_p=0x7fffffffcc08)
    at ../../gcc/gimplify.c:6051
#2  0x0000000000b636a5 in gimplify_expr (expr_p=0x7ffff76682e0, pre_p=0x7fffffffd3f0, post_p=0x7fffffffcc08, gimple_test_f=0xb16def <is_gimple_call_addr(tree_node*)>, fallback=1)
    at ../../gcc/gimplify.c:11581
#3  0x0000000000b4f52c in gimplify_call_expr (expr_p=0x7ffff77eaee0, pre_p=0x7fffffffd3f0, want_value=false)
    at ../../gcc/gimplify.c:3308
#4  0x0000000000b6336e in gimplify_expr (expr_p=0x7ffff77eaee0, pre_p=0x7fffffffd3f0, post_p=0x7fffffffcf58, gimple_test_f=0xb5411f <is_gimple_stmt(tree)>, fallback=0)
    at ../../gcc/gimplify.c:11506
#5  0x0000000000b571dd in gimplify_stmt (stmt_p=0x7ffff77eaee0, seq_p=0x7fffffffd3f0)
    at ../../gcc/gimplify.c:6690
#6  0x0000000000b4bcb9 in gimplify_statement_list (expr_p=0x7ffff743ddd0, pre_p=0x7fffffffd3f0)
    at ../../gcc/gimplify.c:1764
#7  0x0000000000b647a5 in gimplify_expr (expr_p=0x7ffff743ddd0, pre_p=0x7fffffffd3f0, post_p=0x7fffffffd208, gimple_test_f=0xb5411f <is_gimple_stmt(tree)>, fallback=0)
    at ../../gcc/gimplify.c:11963
#8  0x0000000000b571dd in gimplify_stmt (stmt_p=0x7ffff743ddd0, seq_p=0x7fffffffd3f0)
    at ../../gcc/gimplify.c:6690
#9  0x0000000000b4ad3a in gimplify_bind_expr (expr_p=0x7ffff744b1c0, pre_p=0x7fffffffd708)
    at ../../gcc/gimplify.c:1331
#10 0x0000000000b63d56 in gimplify_expr (expr_p=0x7ffff744b1c0, pre_p=0x7fffffffd708, post_p=0x7fffffffd558, gimple_test_f=0xb5411f <is_gimple_stmt(tree)>, fallback=0)
    at ../../gcc/gimplify.c:11735
#11 0x0000000000b571dd in gimplify_stmt (stmt_p=0x7ffff744b1c0, seq_p=0x7fffffffd708)
    at ../../gcc/gimplify.c:6690
#12 0x0000000000b6610a in gimplify_body (fndecl=0x7ffff744b100, do_parms=true)
    at ../../gcc/gimplify.c:12735
#13 0x0000000000b66740 in gimplify_function_tree (fndecl=0x7ffff744b100)
    at ../../gcc/gimplify.c:12900
#14 0x00000000009798ad in cgraph_node::analyze (this=0x7ffff74472e0)
    at ../../gcc/cgraphunit.c:670
#15 0x000000000097aa60 in analyze_functions (first_time=true)
    at ../../gcc/cgraphunit.c:1131
#16 0x000000000097e71a in symbol_table::finalize_compilation_unit (this=0x7ffff7658100)
    at ../../gcc/cgraphunit.c:2691
#17 0x0000000000e79db0 in compile_file ()
    at ../../gcc/toplev.c:480
#18 0x0000000000e7c68a in do_compile ()
    at ../../gcc/toplev.c:2132
#19 0x0000000000e7c966 in toplev::main (this=0x7fffffffda7e, argc=18, argv=0x7fffffffdb88)
    at ../../gcc/toplev.c:2267
#20 0x00000000019e07e6 in main (argc=18, argv=0x7fffffffdb88)
    at ../../gcc/main.c:39

バックトレースの引数表示もうまくいっているようです。

編集者:すずき(2023/09/24 11:53)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年3月30日

GCCを調べる - デバッグ環境再び(ブートストラップモード)

目次: GCC

前回(2021年3月29日の日記参照)はconfigureオプションに --disable-bootstrapを指定してブートストラップモードを無効にしてビルドしました。ブートストラップモードが有効なときについても、メモしておこうと思います。

ブートストラップモードはホストのコンパイラでSTAGE1コンパイラをビルドし、STAGE1コンパイラを使ってSTAGE2とSTAGE3コンパイラをビルドして、ビルド結果に食い違いがないことを比較するモードです。ライブラリのビルドなどに使われる(最終的にインストールされる)のはSTAGE3のコンパイラのようです。3回GCCをビルドするので、ビルド時間は非ブートストラップモードの3倍近い時間がかかります。

ビルドオプションの変え方

ブートストラップモードのときはconfigureにCFLAGS, CXXFLAGSを指定する方法は使えません。代わりに GCCのマニュアルに記載がある通りmake BOOT_CFLAGS="-O0 -g -fno-inline" bootstrapとすれば良いです。

こちらがおそらく正規の手順で、configureにCFLAGS, CXXFLAGSを指定する方法は邪道なんでしょうけど、ブートストラップモードはビルドが遅くて辛いんだよなー……。

最適化をOFFにしてブートストラップモードでGCCをビルド
$ mkdir build
$ cd build

$ ../configure \
  --prefix=`pwd`/_install \
  --enable-languages=c,c++

$ make -j8 BOOT_CFLAGS="-O0 -g -fno-inline" bootstrap
$ make install

ただしBOOT_CFLAGSの指定はSTAGE1には効きません。STAGE1だけは常に手堅い安定したオプションでビルドされます。

STAGE1のCFLAGSは固定

# gcc/Makefile.in

...

# Flags to pass to stage2 and later makes.  They are defined
# here so that they can be overridden by Makefile fragments.
BOOT_CFLAGS= -g -O2
BOOT_LDFLAGS=
BOOT_ADAFLAGS= -gnatpg

...

# Defaults for all stages; some are overridden below.

STAGE_CFLAGS = $(BOOT_CFLAGS)    ★★STAGE_CFLAGS = BOOT_CFLAGS★★
STAGE_TFLAGS = $(TFLAGS)
STAGE_CONFIGURE_FLAGS=@stage2_werror_flag@

# Defaults for stage 1; some are overridden below.
STAGE1_CFLAGS = $(STAGE_CFLAGS)    ★★STAGE1_CFLAGS = STAGE_CFLAGS★★
STAGE1_CXXFLAGS = $(CXXFLAGS)
@if target-libstdc++-v3-bootstrap
# Override the above if we're bootstrapping C++.
STAGE1_CXXFLAGS = $(STAGE1_CFLAGS)
@endif target-libstdc++-v3-bootstrap

...

# By default, C and C++ are the only stage1 languages, because they are the
# only ones we require to build with the bootstrap compiler, and also the
# only ones useful for building stage2.

STAGE1_CFLAGS = @stage1_cflags@    ★★STAGE1_CFLAGSだけ無理やり上書きされる★★
STAGE1_CHECKING = @stage1_checking@
STAGE1_LANGUAGES = @stage1_languages@

当然ですがSTAGE2とSTAGE3には設定が反映されます。STAGE1コンパイラを手動で使って何かビルドする人はほぼいないと思うので、特に問題ないでしょう。

編集者:すずき(2023/09/24 11:54)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年4月2日

GCCを調べる - GCC 8.3のfoldingバグ - 発生条件

目次: GCC

GCC 8.3のバグを追った記録です。来月になったら絶対に忘れて、説明できなくなるので、できる限り詳細にメモしておきたいと思います。

再現条件: コード

再現は簡単で、下記のコードをコンパイルするとinternal compile errorになります。

エラーになるコード

float cargf(float _Complex z);
float atan2f(float y, float x);

void func(float _Complex cval, float val)
{
	__builtin_cargf(cval);
	__builtin_atan2f(val, 1.0f);
}
エラーメッセージ
$ x86_64-unknown-elf-gcc -Wall -O2 -g a.c

a.c: In function 'func':
a.c:7:2: warning: statement with no effect [-Wunused-value]
  __builtin_cargf(cval);
  ^~~~~~~~~~~~~~~~~~~~~
during GIMPLE pass: forwprop
a.c:9:1: internal compiler error: tree check: expected ssa_name, have var_decl in simplify_builtin_call, at tree-ssa-forwprop.c:1246
 }
 ^
0x1331604 tree_check_failed(tree_node const*, char const*, int, char const*, ...)
        ../../gcc/tree.c:9338
0x7ca7f2 tree_check(tree_node*, char const*, int, char const*, tree_code)
        ../../gcc/tree.h:3142
0x1135fb9 simplify_builtin_call
        ../../gcc/tree-ssa-forwprop.c:1246
0x113b55f execute
        ../../gcc/tree-ssa-forwprop.c:2527
Please submit a full bug report,
with preprocessed source if appropriate.
Please include the complete backtrace with any bug report.
See <https://gcc.gnu.org/bugs/> for instructions.

再現にあたり重要なポイントは2点です。

  • carg, atan2を関数宣言する
  • carg, atan2の順に呼ぶ(単独、逆だとエラーにならない)

なぜこの2点が重要か?については、追々説明します。この条件だけで原因が「ああ、あれか」と見当がつく人は超凄いです。GCCマスターか天才ですね。この記事は一切読む必要がないです。ちなみに私は解析に1週間近く掛かりました。辛かったです……。

再現条件: コンパイラ

このエラーはディストリビューションが配布するGCC 8.3のバイナリでは発生しません。x86_64向けのGCCでも発生させるには、下記に示すように特殊なビルド条件にする必要があります。

  • Target triplet(※)のoperatingsystem = elfになっていること(ディストリビューションが配布するバイナリはlinux-gnuなので該当しない)
  • enable-checking=yesでビルドする(ディストリビューションが配布するバイナリはenable-checking=releaseのことが多いので該当しない)

(※)GNUのビルドシステムが使うシステム名の表し方です。machine-vendor-operatingsystemの順で表します。

PC向けでは特殊なビルド条件ですが、ベアメタル向けのクロスコンパイラだと、割とこの条件に当てはまるものは多いです。

ビルドコンフィグ例
$ ../configure \
  --target=x86_64-unknown-elf \
  --prefix=/path/to/gcc/build/_install \
  --disable-bootstrap \
  --disable-libsanitizer \
  --enable-checking=yes \
  --enable-languages="c,c++" \
  CFLAGS="-g -O0 -fno-inline" \
  CXXFLAGS="-g -O0 -fno-inline"

$ make -j8 all-gcc
$ make install-gcc

ビルドコンフィグの一例を示しました。disable-bootstrapはデバッグ用ビルドオプション(CFLAGS, CXXFLAGS)を指定するために使っています(詳しくは 2021年3月30日の日記参照)。disable-libsanitizerは私の環境でビルドエラーになったので、仕方なくビルド対象から外しています。enable-languagesはFortranなどの今回使わない言語を削ってビルド時間を短縮するためです。

デバッグ

デバッグする対象はおなじみcc1です。なぜcc1なのかは以前書いた(2019年5月17日の日記参照)とおりです。

デバッグの例
#### gdbでデバッグするなら

$ gdb /path/to/build/_install/libexec/gcc/x86_64-unknown-elf/8.3.0/cc1

(gdb) run -quiet a.c -mtune=generic -march=x86-64 \
  -g -O2 -Wall -std=c99 -o zzzzzzzz.s


#### gdbserverを使うなら

$ gdbserver --multi localhost:1234 \
  -quiet a.c -mtune=generic -march=x86-64 \
  -g -O2 -Wall -std=c99 -o zzzzzzzz.s

問題の再現と、GCCのコードを追う準備ができました。

編集者:すずき(2023/09/24 11:54)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年4月3日

GCCを調べる - GCC 8.3のfoldingバグ - エラーの原因をvuseから追う

目次: GCC

最初に書いておくと、vuseから追う解析は正解には至りませんでしたが、試行錯誤のあとも一応残しておきます。再現環境とデバッグの準備ができました。エラーが発生する箇所を調べます。

エラー発生箇所

// gcc/gcc/tree-ssa-forwprop.c

static bool
simplify_builtin_call (gimple_stmt_iterator *gsi_p, tree callee2)
{
  gimple *stmt1, *stmt2 = gsi_stmt (*gsi_p);
  tree vuse = gimple_vuse (stmt2);
  if (vuse == NULL)
    return false;
  stmt1 = SSA_NAME_DEF_STMT (vuse);    //★★ここでエラー


// gcc/gcc/tree.h

/* Returns the statement which defines this SSA name.  */
#define SSA_NAME_DEF_STMT(NODE)	SSA_NAME_CHECK (NODE)->ssa_name.def_stmt


// gcc/gcc/tree-check.h

#define SSA_NAME_CHECK(t)	TREE_CHECK (t, SSA_NAME)


// gcc/gcc/tree.h

/* When checking is enabled, errors will be generated if a tree node
   is accessed incorrectly. The macros die with a fatal error.  */
#if defined ENABLE_TREE_CHECKING && (GCC_VERSION >= 2007)

//★★enable-checking=yesだとこちらが有効になるので、エラーが発生する

#define TREE_CHECK(T, CODE) \
(tree_check ((T), __FILE__, __LINE__, __FUNCTION__, (CODE)))

...

#else /* not ENABLE_TREE_CHECKING, or not gcc */

...

//★★enable-checking=releaseだとこちらが有効になるので、エラーが発生しない

#define TREE_CHECK(T, CODE)			(T)


// gcc/gcc/tree.h

//★★
// SSA_NAME_DEF_STMT (vuse)
// TREE_CHECK (vuse, SSA_NAME)
// tree_check (vuse, __FILE__, __LINE__, __FUNCTION__, SSA_NAME)
//
// vuseのTREE_CODEはVAR_DECL

inline tree
tree_check (tree __t, const char *__f, int __l, const char *__g, tree_code __c)
{
  if (TREE_CODE (__t) != __c)  //★★TREE_CODEがSSA_NAMEではないので、このチェックに引っかかる
    tree_check_failed (__t, __f, __l, __g, __c, 0);
  return __t;
}

正直、これを見ても「だから何??」ですよね。

vuseとは?

GCC Internalsを見てもいまいち要領を得ませんが、変数への参照を表しているようです。エラーの原因となっているので、調べるしかありません。どこから来るのでしょうか?

エラー発生箇所

// gcc/gcc/tree-ssa-forwprop.c

static bool
simplify_builtin_call (gimple_stmt_iterator *gsi_p, tree callee2)
{
  //★★stmt2はイテレータgsi_pが指している先頭の要素
  gimple *stmt1, *stmt2 = gsi_stmt (*gsi_p);

  //★★vuseはgimple stmt2をgimple_statement_with_memory_opsにキャストしたときのvuseメンバ
  tree vuse = gimple_vuse (stmt2);

  if (vuse == NULL)
    return false;
  stmt1 = SSA_NAME_DEF_STMT (vuse);    //★★ここでエラー


// gcc/gcc/gimple.h

/* Return the single VUSE operand of the statement G.  */

static inline tree
gimple_vuse (const gimple *g)
{
  const gimple_statement_with_memory_ops *mem_ops_stmt =
     dyn_cast <const gimple_statement_with_memory_ops *> (g);
  if (!mem_ops_stmt)
    return NULL_TREE;
  return mem_ops_stmt->vuse;
}


// gcc/gcc/gimple-iterator.h

/* Return the current stmt.  */

static inline gimple *
gsi_stmt (gimple_stmt_iterator i)
{
  return i.ptr;
}

このvuseメンバを設定するのはどこでしょうか?ソースコードから探すのは困難そうなので、watchpointで探しましょう。

vuseの値とアドレスを調べる
$ gdb /path/to/build/_install/libexec/gcc/x86_64-unknown-elf/8.3.0/cc1

(gdb) r -quiet a.c -mtune=generic -march=x86-64 -g -O2 -Wall -std=c99 -o zzzzzzzz.s

...エラーが出ることを確認する...

(gdb) b tree-ssa-forwprop.c:1246

Breakpoint 1 at 0x11360b5: file ../../gcc/tree-ssa-forwprop.c, line 1246.

(gdb) r

Breakpoint 1, simplify_builtin_call (gsi_p=0x7fffffffd680,
    callee2=0x7ffff74af300) at ../../gcc/tree-ssa-forwprop.c:1246
1246      stmt1 = SSA_NAME_DEF_STMT (vuse);

(gdb) p *stmt2

$2 = {code = GIMPLE_CALL, no_warning = 0, visited = 0, nontemporal_move = 0,
...


★★code = GIMPLE_CALLなのでgcallにキャストしてもう一回ダンプ

(gdb) p *(gcall *)stmt2

$3 = {<gimple_statement_with_memory_ops_base> = {<gimple_statement_with_ops_base> = {<gimple> = {code = GIMPLE_CALL, no_warning = 0, visited = 0,
        nontemporal_move = 0, plf = 0, modified = 0, has_volatile_ops = 0,
        pad = 0, subcode = 0, uid = 0, location = 2147483655, num_ops = 5,
        bb = 0x7ffff7475410, next = 0x7ffff7476118, prev = 0x7ffff7599b90},
      use_ops = 0x7ffff75b14f8}, vdef = 0x7ffff7ffbf30,
    vuse = 0x7ffff7ffbf30}, ★★これ★★
    call_used = {anything = 1, nonlocal = 0,
...


★★stmt2->vuseのアドレスを調べる

(gdb) p &((gcall *)stmt2)->vuse

$4 = (tree *) 0x7ffff75b30c8

エラーが発生するときのvuseは0x7ffff7ffbf30で、vuseを持っている変数 ((gcall *)stmt2)->vuseのアドレスは0x7ffff75b30c8です。デバッグ時、アドレスは毎回同じになることを利用して、先程調べたアドレスにwatchpointを設定し、何か値が書き込まれたら止めます。

stmt2->vuseの書き換え箇所で止める
★★stmt2->vuseを書き換える箇所を特定するためwatchpointを設定する

(gdb) watch *(int *)0x7ffff75b30c8

(gdb) r

...memset系で止まるところは無視...

Old value = 0
New value = -134234320
gimple_set_vuse (g=0x7ffff75b3090, vuse=0x7ffff7ffbf30)
    at ../../gcc/gimple.h:2084
2084    }

それらしき関数gimple_set_vuse() が見つかりました。vuseの値も0x7ffff7ffbf30で関数simplify_builtin_call() で観測した値と一致しており、別の用事で書き換えられたわけではなさそうです。

さらに追っていくと、vuseはcfun->gimple_df->vopが元になっていることがわかり、cfun->gimple_df->vopはcreate_vop_var() によって生成されていることがわかるのですが、そこで行き詰まってしまいます。GCCはエラーメッセージからエラーが発生した箇所はすぐにわかります。しかしエラーの原因はわからないことがほとんどです。GCCのデバッグの辛いところですね。

別のアプローチが必要そうです。

編集者:すずき(2023/09/24 11:54)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年4月4日

GCCを調べる - GCC 8.3のfoldingバグ - GCC 9.1との比較

目次: GCC

実はGCC 9.1ではcarg, atan2を並べてもエラーが発生しませんので、バグがどこかで直っています。GCC 9.1と8.3の動作の違いを調べることで、原因と直し方がわかるはずです。

cargの変換

GIMPLEを出力しながらコードを追っていくと、下記の関数でcargがatan2に変換され、その後internal compile errorになるようです。

cargの変換箇所

// gcc/gcc/builtins.c

/* Fold a call to builtin carg(a+bi) -> atan2(b,a).  */

static tree
fold_builtin_carg (location_t loc, tree arg, tree type)
{
  if (validate_arg (arg, COMPLEX_TYPE)
      && TREE_CODE (TREE_TYPE (TREE_TYPE (arg))) == REAL_TYPE)
    {
      tree atan2_fn = mathfn_built_in (type, BUILT_IN_ATAN2);    //★★この関数がNULL以外を返す条件がある

      if (atan2_fn)
        {
  	  tree new_arg = builtin_save_expr (arg);    //★★ここにくるとcarg → atan2に変換される
	  tree r_arg = fold_build1_loc (loc, REALPART_EXPR, type, new_arg);
	  tree i_arg = fold_build1_loc (loc, IMAGPART_EXPR, type, new_arg);
	  return build_call_expr_loc (loc, atan2_fn, 2, i_arg, r_arg);
	}
    }

  return NULL_TREE;
}

関数mathfn_built_in() はbuiltin_info[uns_fncode].implicit_pがセットされていないとNULLを返す仕組みになっています。

mathfn_built_in() の実装

// gcc/gcc/builtins.c

/* Like mathfn_built_in_1, but always use the implicit array.  */

tree
mathfn_built_in (tree type, combined_fn fn)
{
  return mathfn_built_in_1 (type, fn, /*implicit=*/ 1);    //★★
}

/* Return mathematic function equivalent to FN but operating directly on TYPE,
   if available.  If IMPLICIT_P is true use the implicit builtin declaration,
   otherwise use the explicit declaration.  If we can't do the conversion,
   return null.  */

static tree
mathfn_built_in_1 (tree type, combined_fn fn, bool implicit_p)
{
  built_in_function fcode2 = mathfn_built_in_2 (type, fn);
  if (fcode2 == END_BUILTINS)
    return NULL_TREE;

  if (implicit_p && !builtin_decl_implicit_p (fcode2))    //★★implicit_pフラグがセットされないと変換されない
    return NULL_TREE;

  return builtin_decl_explicit (fcode2);    //★★指定された数学関数のtreeを返す(今回はatan2f)
}


// gcc/gcc/tree.h

/* Return whether the standard builtin function can be used implicitly.  */

static inline bool
builtin_decl_implicit_p (enum built_in_function fncode)
{
  size_t uns_fncode = (size_t)fncode;

  gcc_checking_assert (BUILTIN_VALID_P (fncode));
  return (builtin_info[uns_fncode].decl != NULL_TREE
	  && builtin_info[uns_fncode].implicit_p);    //★★implicit_pフラグがセットされないと変換されない
}


// gcc/gcc/builtins.c

#define CASE_MATHFN(MATHFN) \
  CASE_CFN_##MATHFN: \
  fcode = BUILT_IN_##MATHFN; fcodef = BUILT_IN_##MATHFN##F ; \
  fcodel = BUILT_IN_##MATHFN##L ; break;


/* Return a function equivalent to FN but operating on floating-point
   values of type TYPE, or END_BUILTINS if no such function exists.
   This is purely an operation on function codes; it does not guarantee
   that the target actually has an implementation of the function.  */

static built_in_function
mathfn_built_in_2 (tree type, combined_fn fn)
{
  tree mtype;

...

  switch (fn)
    {
...
    CASE_MATHFN (ATAN)
    CASE_MATHFN (ATAN2)    //★★ここにヒットしてbreak
    CASE_MATHFN (ATANH)
    CASE_MATHFN (CBRT)
... 

    default:
      return END_BUILTINS;
    }

  mtype = TYPE_MAIN_VARIANT (type);
  if (mtype == double_type_node)
    return fcode;
  else if (mtype == float_type_node)
    return fcodef;    //★★ここにくる、返り値はBUILTIN_ATAN2F
  else if (mtype == long_double_type_node)
    return fcodel;
...
  else if (mtype == float128x_type_node)
    return fcodef128x;
  else
    return END_BUILTINS;
}

しかし、cargf → atan2f変換自体はおかしいことではないはずです。

GCC 9.1との比較

GCC 9.1と動作を比較してみます。

GCC 9.1 (OK)GCC 8.3 (NG)
analyze_functions(true) でimplicit_p: false → true analyze_functions(true) でimplicit_p: false → true
pass_lower_cfでcargf → atan2fに置き換え pass_forwpropでcargf → atan2fに置き換え
pass_lower_cfでimplicit_p: true → true pass_forwpropでimplicit_p: true → true
pass_build_ssaでvuseがSSA_NAMEに置き換わる (VAR_DECLのまま)
pass_forwpropでsimplify_builtin_call() pass_forwpropでsimplify_builtin_call() → エラー!!
implicit_p
cargf() をatan2f() に変換するfold_builtin_carg() が発動するかしないかを決めるフラグです。具体的にはbuiltin_info[16].implicit_pです。16はBUILT_IN_ATAN2Fの値です。
simplify_builtin_call()
最初に調べたとおりgimpleのvuseのチェックを行う箇所で、SSA_NAME以外だとinternal compile errorになります。

パスの実行順序は早い順にpass_lower_cf (008t.lower), pass_build_ssa (019t.ssa), pass_forwprop (029t.forwprop1) になります。カッコ内はGCC 9.1でdump-tree-allを指定したときのダンプファイルとの対応です。8.3の場合はpass_forwprop (033t.forwprop) になります。

動作の違いはcargf() → atan2f() の変換が行われるパスです。GCC 9.1はpass_lower_cfですが、GCC 8.3はpass_forwpropです。GCC 9.1は序盤のパスでcargf() → atan2f() の変換が行われるため、pass_build_ssaでvuseが適切に書き換えられて救われるようです。

光明が見えてきました。

編集者:すずき(2023/09/24 11:54)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年4月5日

GCCを調べる - GCC 8.3のfoldingバグ - 解決編

目次: GCC

GCC 9.1との比較でpass_forwpropより早いタイミングでcargf() → atan2f() の置き換えをすれば良さそうであることがわかりました。

さらにGCC 9.1の動作を追うとpass_lower_cfでcargf() → atan2f() の置き換えをしている箇所が、下記の箇所であることがわかります。

pass_lower_cfでcargf() → atan2f() の置き換えを行う方法

// gcc/gcc/gimple-low.c

class pass_lower_cf : public gimple_opt_pass
{
public:
  pass_lower_cf (gcc::context *ctxt)
    : gimple_opt_pass (pass_data_lower_cf, ctxt)
  {}

  /* opt_pass methods: */
  virtual unsigned int execute (function *) { return lower_function_body (); }    //★★これ

}; // class pass_lower_cf


/* Lower the body of current_function_decl from High GIMPLE into Low
   GIMPLE.  */

static unsigned int
lower_function_body (void)
{
  struct lower_data data;
  gimple_seq body = gimple_body (current_function_decl);
  gimple_seq lowered_body;
  gimple_stmt_iterator i;
  gimple *bind;
  gimple *x;

...

  bind = gimple_seq_first_stmt (body);
  lowered_body = NULL;
  gimple_seq_add_stmt (&lowered_body, bind);
  i = gsi_start (lowered_body);
  lower_gimple_bind (&i, &data);    //★★これ

...


/* Lower a bind_expr TSI.  DATA is passed through the recursion.  */

static void
lower_gimple_bind (gimple_stmt_iterator *gsi, struct lower_data *data)
{
  tree old_block = data->block;
  gbind *stmt = as_a <gbind *> (gsi_stmt (*gsi));
  tree new_block = gimple_bind_block (stmt);

...

  lower_sequence (gimple_bind_body_ptr (stmt), data);    //★★これ

...


static void
lower_stmt (gimple_stmt_iterator *gsi, struct lower_data *data)
{
  gimple *stmt = gsi_stmt (*gsi);

  gimple_set_block (stmt, data->block);

  switch (gimple_code (stmt))
    {

...

    case GIMPLE_CALL:
      {
	tree decl = gimple_call_fndecl (stmt);
	unsigned i;

	for (i = 0; i < gimple_call_num_args (stmt); i++)
	  {
	    tree arg = gimple_call_arg (stmt, i);
	    if (EXPR_P (arg))
	      TREE_SET_BLOCK (arg, data->block);
	  }

	if (decl
	    && DECL_BUILT_IN_CLASS (decl) == BUILT_IN_NORMAL)
	  {
	    if (DECL_FUNCTION_CODE (decl) == BUILT_IN_SETJMP)
	      {
		lower_builtin_setjmp (gsi);
		data->cannot_fallthru = false;
		return;
	      }
	    else if (DECL_FUNCTION_CODE (decl) == BUILT_IN_POSIX_MEMALIGN
		     && flag_tree_bit_ccp
		     && gimple_builtin_call_types_compatible_p (stmt, decl))
	      {
		lower_builtin_posix_memalign (gsi);
		return;
	      }
	  }

	if (decl && (flags_from_decl_or_type (decl) & ECF_NORETURN))
	  {
	    data->cannot_fallthru = true;
	    gsi_next (gsi);
	    return;
	  }

	  //★★
	  //GCC 9.1だと下記のようなfold_stmt() を呼ぶ処理があるが、GCC 8.3には存在しない
	  //GCC 8.3に同様の処理を足すときは #include "gimple-fold.h" も足す必要がある

	  /* We delay folding of built calls from gimplification to
	     here so the IL is in consistent state for the diagnostic
	     machineries job.  */
	  if (gimple_call_builtin_p (stmt))
	    fold_stmt (gsi);
      }
      break;

...

この変更を加えるとinternal compile errorが出なくなります。

発生条件の理由

最初に、このエラーの発生条件の1つとしてTarget triplet(※)のoperatingsystem = elfになっていることを挙げました。

理由はoperationgsystem = linuxにするとimplicit_pが最初から全てセットされた状態でコンパイル処理が始まるからです。GCC 8.3でもpass_forwpropより早い段階でcargf() → atan2f() の置き換えが発生し、バグを隠してしまいます。

elfとlinuxのときのimplicit_pの違い

// gcc/gcc/builtins.def

/* Builtin that is specified by C99 and C90 reserve the name for future use.
   We can still recognize the builtin in C90 mode but we can't produce it
   implicitly.  */
#undef DEF_C99_C90RES_BUILTIN
#define DEF_C99_C90RES_BUILTIN(ENUM, NAME, TYPE, ATTRS)	\
  DEF_BUILTIN (ENUM, "__builtin_" NAME, BUILT_IN_NORMAL, TYPE, TYPE,	\
	       true, true, !flag_isoc99, ATTRS, targetm.libc_has_function (function_c99_misc), true)

...

/* Category: math builtins.  */
DEF_LIB_BUILTIN        (BUILT_IN_ACOS, "acos", BT_FN_DOUBLE_DOUBLE, ATTR_MATHFN_FPROUNDING_ERRNO)
DEF_C99_C90RES_BUILTIN (BUILT_IN_ACOSF, "acosf", BT_FN_FLOAT_FLOAT, ATTR_MATHFN_FPROUNDING_ERRNO)

...

//★★atan2fはfn_class = function_c99_miscを渡してlibc_has_function() を呼ぶ★★


// gcc/gcc/config/linux.h

/* Determine what functions are present at the runtime;
   this includes full c99 runtime and sincos.  */
# undef TARGET_LIBC_HAS_FUNCTION
# define TARGET_LIBC_HAS_FUNCTION linux_libc_has_function


// gcc/gcc/config/linux.c

bool
linux_libc_has_function (enum function_class fn_class)
{
  if (OPTION_GLIBC || OPTION_MUSL)
    return true;
  if (OPTION_BIONIC)
    if (fn_class == function_c94
	|| fn_class == function_c99_misc
	|| fn_class == function_sincos)
	return true;    //★★operatingsystem = linuxのときはacosfのimplicit_pの初期値はtrue

  return false;
}


// gcc/gcc/config/elfos.h

#undef TARGET_LIBC_HAS_FUNCTION
#define TARGET_LIBC_HAS_FUNCTION no_c99_libc_has_function


// gcc/gcc/targhooks.c

bool
no_c99_libc_has_function (enum function_class fn_class ATTRIBUTE_UNUSED)
{
  return false;    //★★operatingsystem = elfのときはimplicit_pの初期値はfalse
}

最初は、この発生条件に気づかなくてGCC 8.3のバグだと気づくのがだいぶ遅れました……。

参考

関数cargf() やatan2f() はビルトイン関数のため、何も宣言しなくても関数の実体が存在します。しかし最適化を有効にするには、あえて関数宣言する必要があります。関数宣言しないとimplicit_pフラグがセットされない仕組みになっているからです。

analyze_functions(true) からimplicit_pの設定までの経路

// gcc/gcc/cgraphunit.c

void
symbol_table::finalize_compilation_unit (void)
{

...

  /* Gimplify and lower all functions, compute reachability and
     remove unreachable nodes.  */
  analyze_functions (/*first_time=*/true);    //★★これ

...


static void
analyze_functions (bool first_time)
{
  /* Keep track of already processed nodes when called multiple times for
     intermodule optimization.  */
  cgraph_node *first_handled = first_analyzed;
  varpool_node *first_handled_var = first_analyzed_var;
  hash_set<void *> reachable_call_targets;

  symtab_node *node;
  symtab_node *next;
  int i;
  ipa_ref *ref;
  bool changed = true;
  location_t saved_loc = input_location;

...

  /* Analysis adds static variables that in turn adds references to new functions.
     So we need to iterate the process until it stabilize.  */
  while (changed)
    {

...

      /* Lower representation, build callgraph edges and references for all trivially
         needed symbols and all symbols referred by them.  */
      while (queued_nodes != &symtab_terminator)
	{
	  changed = true;
	  node = queued_nodes;
	  queued_nodes = (symtab_node *)queued_nodes->aux;
	  cgraph_node *cnode = dyn_cast <cgraph_node *> (node);

...

	      if (!cnode->analyzed)
		cnode->analyze ();    //★★これ

...


/* Analyze the function scheduled to be output.  */
void
cgraph_node::analyze (void)
{
  if (native_rtl_p ())
    {
      analyzed = true;
      return;
    }

  tree decl = this->decl;
  location_t saved_loc = input_location;
  input_location = DECL_SOURCE_LOCATION (decl);

...


  if (alias)
    resolve_alias (cgraph_node::get (alias_target), transparent_alias);
  else if (dispatcher_function)
    {
...
    }
  else
    {
      push_cfun (DECL_STRUCT_FUNCTION (decl));

      assign_assembler_name_if_needed (decl);

      /* Make sure to gimplify bodies only once.  During analyzing a
	 function we lower it, which will require gimplified nested
	 functions, so we can end up here with an already gimplified
	 body.  */
      if (!gimple_has_body_p (decl))
	gimplify_function_tree (decl);    //★★これ

...


// gcc/gcc/gimplify.c

/* Entry point to the gimplification pass.  FNDECL is the FUNCTION_DECL
   node for the function we want to gimplify.

   Return the sequence of GIMPLE statements corresponding to the body
   of FNDECL.  */
void
gimplify_function_tree (tree fndecl)
{
  tree parm, ret;
  gimple_seq seq;
  gbind *bind;

...

  bind = gimplify_body (fndecl, true);    //★★これ

...


/* Gimplify the body of statements of FNDECL and return a GIMPLE_BIND node
   containing the sequence of corresponding GIMPLE statements.  If DO_PARMS
   is true, also gimplify the parameters.  */

gbind *
gimplify_body (tree fndecl, bool do_parms)
{
  location_t saved_location = input_location;
  gimple_seq parm_stmts, parm_cleanup = NULL, seq;
  gimple *outer_stmt;
  gbind *outer_bind;
  struct cgraph_node *cgn;

...

  /* Gimplify the function's body.  */
  seq = NULL;
  gimplify_stmt (&DECL_SAVED_TREE (fndecl), &seq);    //★★これ

...


/* Gimplify an expression which appears at statement context.  The
   corresponding GIMPLE statements are added to *SEQ_P.  If *SEQ_P is
   NULL, a new sequence is allocated.

   Return true if we actually added a statement to the queue.  */

bool
gimplify_stmt (tree *stmt_p, gimple_seq *seq_p)
{
  gimple_seq_node last;

  last = gimple_seq_last (*seq_p);
  gimplify_expr (stmt_p, seq_p, NULL, is_gimple_stmt, fb_none);    //★★これ
  return last != gimple_seq_last (*seq_p);
}


enum gimplify_status
gimplify_expr (tree *expr_p, gimple_seq *pre_p, gimple_seq *post_p,
	       bool (*gimple_test_f) (tree), fallback_t fallback)
{

...

  /* Loop over the specific gimplifiers until the toplevel node
     remains the same.  */
  do
    {

...

      /* Make sure that all the cases set 'ret' appropriately.  */
      ret = GS_UNHANDLED;
      switch (TREE_CODE (*expr_p))
	{

...

	case BIND_EXPR:
	  ret = gimplify_bind_expr (expr_p, pre_p);    //★★これ
	  break;

...


//★★以降は
// gimplify_bind_expr
// gimplify_stmt
//
// gimplify_expr
// gimplify_statement_list
// gimplify_stmt
//
// gimplify_expr
// gimplify_call_expr
//
// gimplify_expr
// gimplify_addr_expr
//
// こんな感じ


/* Rewrite the ADDR_EXPR node pointed to by EXPR_P

      unary_expr
	      : ...
	      | '&' varname
	      ...

    PRE_P points to the list where side effects that must happen before
	*EXPR_P should be stored.

    POST_P points to the list where side effects that must happen after
	*EXPR_P should be stored.  */

static enum gimplify_status
gimplify_addr_expr (tree *expr_p, gimple_seq *pre_p, gimple_seq *post_p)
{
  tree expr = *expr_p;
  tree op0 = TREE_OPERAND (expr, 0);
  enum gimplify_status ret;
  location_t loc = EXPR_LOCATION (*expr_p);

  switch (TREE_CODE (op0))
    {

...

    default:
      /* If we see a call to a declared builtin or see its address
	 being taken (we can unify those cases here) then we can mark
	 the builtin for implicit generation by GCC.  */
      if (TREE_CODE (op0) == FUNCTION_DECL
	  && DECL_BUILT_IN_CLASS (op0) == BUILT_IN_NORMAL
	  && builtin_decl_declared_p (DECL_FUNCTION_CODE (op0)))
	set_builtin_decl_implicit_p (DECL_FUNCTION_CODE (op0), true);    //★★これ

...


//★★このif文が成立するにはcargfやatan2f関数を宣言する必要がある。
//
// TREE_CODE (op0) == FUNCTION_DECL
// DECL_BUILT_IN_CLASS (op0) == BUILT_IN_NORMAL
// builtin_decl_declared_p (DECL_FUNCTION_CODE (op0)): cargfやatan2fを関数宣言しないとtrueにならない
//
// ソースコードから、下記の宣言を削除すると、
//
// float cargf(float _Complex z);
// float atan2f(float y, float x);
//
// TREE_CODE (op0) == FUNCTION_DECL                 : 1
// DECL_BUILT_IN_CLASS (op0) == BUILT_IN_NORMAL     : 1
// builtin_decl_declared_p (DECL_FUNCTION_CODE (op0): 0


// gcc/gcc/tree.h

/* Set the implicit flag for a builtin function.  */

static inline void
set_builtin_decl_implicit_p (enum built_in_function fncode, bool implicit_p)
{
  size_t uns_fncode = (size_t)fncode;

  gcc_checking_assert (BUILTIN_VALID_P (fncode)
		       && builtin_info[uns_fncode].decl != NULL_TREE);

  builtin_info[uns_fncode].implicit_p = implicit_p;    //★★implicit_pが書き換わる
}

ソースコードからcargf() やatan2f() の宣言を削除すると、implicit_pがセットされなくなってcargf() をatan2f() に置き換える最適化が発動しなくなります。cargf() が存在しないような環境(C89など)のためですかね……?

編集者:すずき(2023/09/24 11:55)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年4月6日

ディスプレイアーム

机の奥行きが60cmのためか、ディスプレイの足がキーボードとぶつかって若干邪魔なのと、前からディスプレイアームに興味があったので、ディスプレイアームを買いました。

買ったのはergotron LXデスクマウントアーム(品番45-241-026、製品サイトへのリンクです。Amazonで11,000円くらいでした。1つだけディスプレイがつけられるタイプです。我が家はデュアルディスプレイなので、2つアームを買いました。

1つのアームに2つのディスプレイを付けられるタイプ(品番45-245-026、製品サイトへのリンク)もありますが、なぜかアーム2つ分より高かったことと、1か所に荷重が掛かりすぎて机が壊れたら嫌だなと思って購入は控えました。

設置

アーム+ディスプレイをクランプでテーブルの天板に止めるだけの構造です。天板の強度が不安だったので、サンワサプライの補強プレートCR-LAPLT1を「裏側」につけています。


天板の表側


天板の裏側(補強プレート)

こんな感じです。デスクマウントアームに限らず、この手のクランプで固定するタイプの製品ならば作りは同じだと思いますが、表側は幅広の足でも、裏側は足が小さく、クランプを全力で締めると天板が凹んだり穴が開いてしまいます。

邪魔にならないように机の端ギリギリに付けたところ、アームが机の真ん中まで届かず、ディスプレイとディスプレイの間が妙に空いてしまいました。見た目がイマイチですね。まあいいか……。

使用感

重い頑丈そうなアームだけあって、安定感は素晴らしいです。

どの方向にもスイーっと動くのかと思っていたのですが、方向によりますね。軸回りに回転させるときは軽いですが、奥行方向、上下に動かすときはかなり力が要ります。逆に言えば手がぶつかった程度では動かないです。私はあまりディスプレイを動かさないのでこれくらい固いほうが良いです。

説明書によると突っ張り具合は調節可能らしいので、頻繁に動かしたい、固すぎて疲れる方は調整すると良いかもしれません。

編集者:すずき(2021/04/12 11:18)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



link もっと前
2021年3月13日 >>> 2021年4月9日
link もっと後

管理用メニュー

link 記事を新規作成

<2021>
<<<03>>>
-123456
78910111213
14151617181920
21222324252627
28293031---

最近のコメント5件

  • link 24年6月17日
    すずきさん (06/23 00:12)
    「ありがとうございます。バルコニーではない...」
  • link 24年6月17日
    hdkさん (06/22 22:08)
    「GPSの最初の同期を取る時は見晴らしのい...」
  • link 24年5月16日
    すずきさん (05/21 11:41)
    「あー、確かにdpkg-reconfigu...」
  • link 24年5月16日
    hdkさん (05/21 08:55)
    「システム全体のlocale設定はDebi...」
  • link 24年5月17日
    すずきさん (05/20 13:16)
    「そうですねえ、普通はStandardなの...」

最近の記事3件

  • link 24年6月27日
    すずき (06/30 15:39)
    「[何もない組み込み環境でDOOMを動かす - その4 - 自作OSの組み込み環境へ移植] 目次: RISC-V目次: 独自OS...」
  • link 22年12月13日
    すずき (06/30 15:38)
    「[独自OS - まとめリンク] 目次: 独自OS一覧が欲しくなったので作りました。自作OSの紹介その1 - 概要自作OSの紹介...」
  • link 21年6月18日
    すずき (06/29 22:28)
    「[RISC-V - まとめリンク] 目次: RISC-VSiFive社ボードの話、CoreMarkの話のまとめ。RISC-V ...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www.katsuster.net
RDFファイル RSS 1.0

最終更新: 06/30 15:39