2020年11月17日

GCC10の最適化によるmemsetでの無限ループの発生

以前、「GCCの最適化による予期せぬ無限ループの発生」という記事を書きました。この時は -fno-builtin-malloc や __asm __volatile("":::"memory"); などで対策できました。

しかし今回、現状最新の GCC 10 で、memset、しかもナイーブな *(char *)s++ = (char)c; みたいな実装ではなく、NetBSD の本格的な実装のもので発生し、-fno-builtin や -fno-builtin-memset、-ffreestanding などでも抑制できず、-fno-tree-loop-distribute-patterns というあまり一般的ではないオプションが必要になりました。

これは一見 GCC のオプションが効いてない、バグのように思えますが、調べて見ると GCC の仕様に根差した問題であることがわかりました。



以下は、NetBSD 9.1 の /usr/src/common/lib/libc/string/memset.c を単体でコンパイルできるように私が修正して、memset と無関係な部分を削除したものです。
/*	$NetBSD: memset.c,v 1.12 2019/03/30 10:18:03 jmcneill Exp $	*/

/*-
 * Copyright (c) 1990, 1993
 *	The Regents of the University of California.  All rights reserved.
 *
 * This code is derived from software contributed to Berkeley by
 * Mike Hibler and Chris Torek.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <limits.h>
#include <string.h>

typedef unsigned char u_char;
typedef unsigned int u_int;
typedef unsigned long u_long;

#define	wsize	sizeof(u_int)
#define	wmask	(wsize - 1)

#undef memset

#define	RETURN	return (dst0)
#define	VAL	c0
#define	WIDEVAL	c

void *
memset(void *dst0, int c0, size_t length)
{
	size_t t;
	u_int c;
	u_char *dst;

	dst = dst0;
	/*
	 * If not enough words, just fill bytes.  A length >= 2 words
	 * guarantees that at least one of them is `complete' after
	 * any necessary alignment.  For instance:
	 *
	 *	|-----------|-----------|-----------|
	 *	|00|01|02|03|04|05|06|07|08|09|0A|00|
	 *	          ^---------------------^
	 *		 dst		 dst+length-1
	 *
	 * but we use a minimum of 3 here since the overhead of the code
	 * to do word writes is substantial.
	 */
	if (length < 3 * wsize) {
		while (length != 0) {
			*dst++ = VAL;
			--length;
		}
		RETURN;
	}

	if ((c = (u_char)c0) != 0) {	/* Fill the word. */
		c = (c << 8) | c;	/* u_int is 16 bits. */
#if UINT_MAX > 0xffff
		c = (c << 16) | c;	/* u_int is 32 bits. */
#endif
#if UINT_MAX > 0xffffffff
		c = (c << 32) | c;	/* u_int is 64 bits. */
#endif
	}
	/* Align destination by filling in bytes. */
	if ((t = (size_t)((u_long)dst & wmask)) != 0) {
		t = wsize - t;
		length -= t;
		do {
			*dst++ = VAL;
		} while (--t != 0);
	}

	/* Fill words.  Length was >= 2*words so we know t >= 1 here. */
	t = length / wsize;
	do {
		*(u_int *)(void *)dst = WIDEVAL;
		dst += wsize;
	} while (--t != 0);

	/* Mop up trailing bytes, if any. */
	t = length & wmask;
	if (t != 0)
		do {
			*dst++ = VAL;
		} while (--t != 0);
	RETURN;
}
これを GCC 6.4.0 で、Cortex-M4 や M7 向け(armv7e-m)にコンパイルすると、以下のようになり、特に問題ありません。
$ arm-none-eabi-gcc -S -O2 -Wall -mthumb -march=armv7e-m memset.c
	.arch armv7e-m
	.eabi_attribute 20, 1
	.eabi_attribute 21, 1
	.eabi_attribute 23, 3
	.eabi_attribute 24, 1
	.eabi_attribute 25, 1
	.eabi_attribute 26, 1
	.eabi_attribute 30, 2
	.eabi_attribute 34, 1
	.eabi_attribute 18, 4
	.file	"memset.c"
	.text
	.align	1
	.p2align 2,,3
	.global	memset
	.syntax unified
	.thumb
	.thumb_func
	.fpu softvfp
	.type	memset, %function
memset:
	@ args = 0, pretend = 0, frame = 0
	@ frame_needed = 0, uses_anonymous_args = 0
	@ link register save eliminated.
	cmp	r2, #11
	bhi	.L2
	cbz	r2, .L24
	uxtb	r1, r1
	add	r2, r2, r0
	mov	r3, r0
.L4:
	strb	r1, [r3], #1
	cmp	r3, r2
	bne	.L4
	bx	lr
.L2:
	ands	r1, r1, #255
	push	{r4, r5}
	itte	ne
	orrne	r5, r1, r1, lsl #8
	orrne	r5, r5, r5, lsl #16
	moveq	r5, r1
	ands	r4, r0, #3
	bne	.L26
	mov	r3, r0
.L7:
	bic	r4, r2, #3
	add	r4, r4, r3
.L9:
	str	r5, [r3], #4
	cmp	r4, r3
	bne	.L9
	ands	r2, r2, #3
	beq	.L14
	add	r2, r2, r3
.L10:
	strb	r1, [r3], #1
	cmp	r3, r2
	bne	.L10
.L14:
	pop	{r4, r5}
.L24:
	bx	lr
.L26:
	rsb	r4, r4, #4
	subs	r2, r2, r4
	mov	r3, r0
	add	r4, r4, r0
.L8:
	strb	r1, [r3], #1
	cmp	r4, r3
	bne	.L8
	b	.L7
	.size	memset, .-memset
	.ident	"GCC: (GNU) 6.4.0"
ところが、全く同じように GCC 10.2.0 でコンパイルすると、以下のように memset から memset を呼び出してしまい無限ループするコードが生成されてしまいます。
	.arch armv7e-m
	.eabi_attribute 20, 1
	.eabi_attribute 21, 1
	.eabi_attribute 23, 3
	.eabi_attribute 24, 1
	.eabi_attribute 25, 1
	.eabi_attribute 26, 1
	.eabi_attribute 30, 2
	.eabi_attribute 34, 1
	.eabi_attribute 18, 4
	.file	"memset.c"
	.text
	.align	1
	.p2align 2,,3
	.global	memset
	.syntax unified
	.thumb
	.thumb_func
	.fpu softvfp
	.type	memset, %function
memset:
	@ args = 0, pretend = 0, frame = 0
	@ frame_needed = 0, uses_anonymous_args = 0
	cmp	r2, #11
	push	{r4, r5, r6, r7, r8, lr}
	mov	r5, r2
	mov	r7, r0
	bls	.L17
	ands	r8, r1, #255
	itte	ne
	orrne	r6, r8, r8, lsl #8
	orrne	r6, r6, r6, lsl #16
	moveq	r6, r8
	ands	r3, r0, #3
	bne	.L18
.L6:
	bic	r3, r5, #3
	add	r3, r3, r0
.L7:
	str	r6, [r0], #4
	cmp	r0, r3
	bne	.L7
	ands	r2, r5, #3
	beq	.L11
	mov	r1, r8
	bl	memset
.L11:
	mov	r0, r7
	pop	{r4, r5, r6, r7, r8, pc}
.L18:
	rsb	r4, r3, #4
	subs	r5, r2, #4
	mov	r1, r8
	mov	r2, r4
	add	r5, r5, r3
	bl	memset
	adds	r0, r7, r4
	b	.L6
.L17:
	cmp	r2, #0
	beq	.L11
	uxtb	r1, r1
	bl	memset
	mov	r0, r7
	pop	{r4, r5, r6, r7, r8, pc}
	.size	memset, .-memset
	.ident	"GCC: (GNU) 10.2.0"
最初、これは GCC のバグだと思ったのですが、某所で okuoku さん(https://qiita.com/okuoku) に教えてもらった所によると、GCC の freestanding 環境(暗黙的に -fno-builtin も有効化される)でも以下の 4 関数は必ず要求され、この関数の呼び出しの生成を抑制することはできない、という仕様が根本原因のようです。つまり、ビルトイン関数の最適化の問題ではなく、コード生成の問題だったので、-fno-builtin 系オプションで抑制できなかったのはバグではなく仕様というわけです。

https://gcc.gnu.org/onlinedocs/gcc/Standards.html
GCC requires the freestanding environment provide memcpy, memmove, memset and memcmp.
対策としては、以下に挙げられているように -fno-tree-loop-distribute-patterns が有効であることを確認しました。これは O2 で有効になる最適化なので、O では大丈夫だったのも道理です。このオプションは、memset 呼び出しの生成を抑制するので、影響は少ない反面、それ以外にまた似たような問題が発生した時は対応できない、あくまでも memset 限定の対応であるという問題はありますが。

https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#index-ftree-loop-distribute-patterns
$ arm-none-eabi-gcc -S -O2 -fno-tree-loop-distribute-patterns -Wall -mthumb -march=armv7e-m memset.c
	.arch armv7e-m
	.eabi_attribute 20, 1
	.eabi_attribute 21, 1
	.eabi_attribute 23, 3
	.eabi_attribute 24, 1
	.eabi_attribute 25, 1
	.eabi_attribute 26, 1
	.eabi_attribute 30, 2
	.eabi_attribute 34, 1
	.eabi_attribute 18, 4
	.file	"memset.c"
	.text
	.align	1
	.p2align 2,,3
	.global	memset
	.syntax unified
	.thumb
	.thumb_func
	.fpu softvfp
	.type	memset, %function
memset:
	@ args = 0, pretend = 0, frame = 0
	@ frame_needed = 0, uses_anonymous_args = 0
	@ link register save eliminated.
	cmp	r2, #11
	bls	.L25
	ands	r1, r1, #255
	push	{r4}
	itte	ne
	orrne	r4, r1, r1, lsl #8
	orrne	r4, r4, r4, lsl #16
	moveq	r4, r1
	ands	r3, r0, #3
	bne	.L26
	mov	r3, r0
.L7:
	bic	ip, r2, #3
	add	ip, ip, r3
.L9:
	str	r4, [r3], #4
	cmp	r3, ip
	bne	.L9
	ands	r2, r2, #3
	beq	.L14
	add	r2, r2, ip
.L10:
	strb	r1, [ip], #1
	cmp	r2, ip
	bne	.L10
.L14:
	pop	{r4}
	bx	lr
.L26:
	subs	r2, r2, #4
	add	r2, r2, r3
	rsb	r3, r3, #4
	add	r3, r3, r0
	mov	ip, r0
.L8:
	strb	r1, [ip], #1
	cmp	ip, r3
	bne	.L8
	b	.L7
.L25:
	cbz	r2, .L22
	uxtb	r1, r1
	add	r2, r2, r0
	mov	r3, r0
.L4:
	strb	r1, [r3], #1
	cmp	r2, r3
	bne	.L4
	bx	lr
.L22:
	bx	lr
	.size	memset, .-memset
	.ident	"GCC: (GNU) 10.2.0"
GCC 4.8 の時代からもう 7 年以上ずっと問題が残り続けているようなので、今後も改善される可能性は低そうです。

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888

kmckk at 17:57コメント(0)GCC | 若槻 

コメントする

名前
 
  絵文字
 
 
記事検索
最新コメント
アクセスカウンター
  • 今日:
  • 昨日:
  • 累計:

QRコード
QRコード