4 ms·
GCC is sort of infamously pedantic about -Os. This is my favorite example: {0}[calvinow ~] cat test.c int foo(int a) { return a / 4; } {0}[ca
by jcalvinowens 3y ago
GCC is sort of infamously pedantic about -Os. This is my favorite example:
{0}[calvinow ~] cat test.c
int foo(int a)
{
return a / 4;
}
{0}[calvinow ~] gcc -c -O2 test.c -o opt.o
{0}[calvinow ~] objdump -d opt.o
opt.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <foo>:
0: 85 ff test %edi,%edi
2: 8d 47 03 lea 0x3(%rdi),%eax
5: 0f 49 c7 cmovns %edi,%eax
8: c1 f8 02 sar $0x2,%eax
b: c3 ret
{0}[calvinow ~] gcc -c -Os test.c -o size.o
{0}[calvinow ~] objdump -d size.o
size.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <foo>:
0: 89 f8 mov %edi,%eax
2: b9 04 00 00 00 mov $0x4,%ecx
7: 99 cltd
8: f7 f9 idiv %ecx
a: c3 ret
...because the division saves a single byte on x86! It's hard to believe anybody would ever want that trade.
- Narishma 3y agoI don't see the problem. You explicitly asked it to generate the smallest code it can and it did.
- jcalvinowens 3y agoIt's a problem because it's practically useless: almost nobody writing C ever actually wants to optimize for size at the cost of everything else. This isn't an uncommon opinion, people have been complaining about -Os for a decade: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=281dc5c5ec0fb299514567cbc358562649c1af95 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin... https://lwn.net/Articles/534735/ https://lwn.net/Articles/534735/
- jcalvinowens 3y agoClang is much more reasonable, see: {0}[calvinow ~] clang -Os -S test.c -c -o - .text .file "test.c" .globl foo # -- Begin function foo .type foo,@function foo: # @foo .cfi_startproc # %bb.0: # kill: def $edi killed $edi def $rdi leal 3(%rdi), %eax testl %edi, %edi cmovnsl %edi, %eax sarl $2, %eax retq .Lfunc_end0: .size foo, .Lfunc_end0-foo .cfi_endproc # -- End function .ident "Debian clang version 16.0.6 (19)" .section ".note.GNU-stack","",@progbits .addrsig {0}[calvinow ~] clang -Oz -S test.c -c -o - .text .file "test.c" .globl foo # -- Begin function foo .type foo,@function foo: # @foo .cfi_startproc # %bb.0: movl %edi, %eax pushq $4 .cfi_adjust_cfa_offset 8 popq %rcx .cfi_adjust_cfa_offset -8 cltd idivl %ecx retq .Lfunc_end0: .size foo, .Lfunc_end0-foo .cfi_endproc # -- End function .ident "Debian clang version 16.0.6 (19)" .section ".note.GNU-stack","",@progbits .addrsig {0}[calvinow ~] clang -O2 -S test.c -c -o - .text .file "test.c" .globl foo # -- Begin function foo .p2align 4, 0x90 .type foo,@function foo: # @foo .cfi_startproc # %bb.0: # kill: def $edi killed $edi def $rdi leal 3(%rdi), %eax testl %edi, %edi cmovnsl %edi, %eax sarl $2, %eax retq .Lfunc_end0: .size foo, .Lfunc_end0-foo .cfi_endproc # -- End function .ident "Debian clang version 16.0.6 (19)" .section ".note.GNU-stack","",@progbits .addrsig GCC has -Oz now, but they haven't fixed -Os to be softer like clang's is.
- kragen 3y agousually if i'm writing c instead of js, python, or golang the reason is that i want my code to run on a microcontroller. the majority of the microcontrollers i have here have 4 kibibytes of flash, which is a pretty harsh limit, but vastly more cpu power than is needed for the things i'm doing with them, so i almost always actually want to optimize for size at the cost of nearly everything else. the only thing that's more precious than flash is ram (why don't i write assembly? well, sometimes i do, but sometimes c is good enough, and it has major advantages. size-optimizing compilers greatly expand the scope of c's applicability.)
- jcalvinowens 3y agoIn theory, -Oz now serves the "smaller no matter what the cost" case, and allows -Os to make compromises. But with GCC, they still behave like synonyms in my limited experience.
- kragen 3y agoi appreciate the new information! i haven't used clang much for reasons that are probably not very good probably bytecode interpreters are a better solution for this sort of thing anyway, at least when you have more than 512 bytes of program memory or so. in forth with gforth locals the function is : sumarray { a n } 0 n 0 ?do a i cells + @ + loop ; and the body (after the }) consists of 12 instructions, almost all of which would be a single byte each in compact bytecodes like smalltalk-80 or emacs lisp. this might even beat clang's 18-byte rvc version above. there's a sequence of three of them that would conventionally be a single operation (cells + @), but conventionally you'd also use extra instructions to load and store the variable `total` inside and outside the loop instead of leaving it on the stack, so you'd end up with 13 instructions. and the ?do and loop instructions normally have an offset, so you're at 15 bytes, plus the subroutine header gforth of course uses 8 bytes per instruction and doesn't have a structure like smalltalk's method header to compactly encode the number of locals, so gforth itself is not very frugal with code space; instead { a n } compiles to >l >l at the beginning and lp+2 at the end
- 3y ago
- kragen 3y agoi've definitely written a lot of assembly code by hand where i did far, far more perverted things to save a single byte; implementing integer division with an integer division instruction is quite vanilla but in my risc-v case the code it emitted with -Os was not only larger than the code it emitted with -Os given hand-optimized input; it was also larger than the code it emitted with the same input and -O3. my complaint wasn't that the code was slow
- jcalvinowens 3y ago> i've definitely written a lot of assembly code by hand where i did far, far more perverted things to save a single byte I mean, obviously, me too. But this is a general purpose compiler: emitting a runtime division by a compile time constant power-of-two is just silly. If you actually care about saving one byte of text and you're writing C, you're doing it wrong. As you point out, it's pretty trivial to beat the compiler most of the time. See below, clang does what I want here.
- kragen 3y ago(no longer below, now it's above)