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 の仕様に根差した問題であることがわかりました。
しかし今回、現状最新の 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 と無関係な部分を削除したものです。
https://gcc.gnu.org/onlinedocs/gcc/Standards.html
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#index-ftree-loop-distribute-patterns
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888
/* $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