Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

gcc's __builtin_memcpy performance with certain number of bytes is terrible compared to clang's

I thought I`d first share this here to have your opinions before doing anything else. I found out while designing an algorithm that the gcc compiled code performance for some simple code was catastrophic compared to clang's.

How to reproduce

Create a test.c file containing this code :

#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

int main(int argc, char *argv[]) {
    const uint64_t size = 1000000000;
    const size_t alloc_mem = size * sizeof(uint8_t);
    uint8_t *mem = (uint8_t*)malloc(alloc_mem);
    for (uint_fast64_t i = 0; i < size; i++)
        mem[i] = (uint8_t) (i >> 7);

    uint8_t block = 0;
    uint_fast64_t counter = 0;
    uint64_t total = 0x123456789abcdefllu;
    uint64_t receiver = 0;

    for(block = 1; block <= 8; block ++) {
        printf("%u ...\n", block);
        counter = 0;
        while (counter < size - 8) {
            __builtin_memcpy(&receiver, &mem[counter], block);
            receiver &= (0xffffffffffffffffllu >> (64 - ((block) << 3)));
            total += ((receiver * 0x321654987cbafedllu) >> 48);
            counter += block;
        }
    }

    printf("=> %llu\n", total);
    return EXIT_SUCCESS;
}

gcc

Compile and run :

gcc-7 -O3 test.c
time ./a.out
1 ...
2 ...
3 ...
4 ...
5 ...
6 ...
7 ...
8 ...
=> 82075168519762377

real    0m23.367s
user    0m22.634s
sys 0m0.495s

info :

gcc-7 -v
Using built-in specs.
COLLECT_GCC=gcc-7
COLLECT_LTO_WRAPPER=/usr/local/Cellar/gcc/7.3.0/libexec/gcc/x86_64-apple-darwin17.4.0/7.3.0/lto-wrapper
Target: x86_64-apple-darwin17.4.0
Configured with: ../configure --build=x86_64-apple-darwin17.4.0 --prefix=/usr/local/Cellar/gcc/7.3.0 --libdir=/usr/local/Cellar/gcc/7.3.0/lib/gcc/7 --enable-languages=c,c++,objc,obj-c++,fortran --program-suffix=-7 --with-gmp=/usr/local/opt/gmp --with-mpfr=/usr/local/opt/mpfr --with-mpc=/usr/local/opt/libmpc --with-isl=/usr/local/opt/isl --with-system-zlib --enable-checking=release --with-pkgversion='Homebrew GCC 7.3.0' --with-bugurl=https://github.com/Homebrew/homebrew-core/issues --disable-nls
Thread model: posix
gcc version 7.3.0 (Homebrew GCC 7.3.0)

So we get about 23s of user time. Now let's do the same with cc (clang on macOS) :

clang

cc -O3 test.c
time ./a.out
1 ...
2 ...
3 ...
4 ...
5 ...
6 ...
7 ...
8 ...
=> 82075168519762377

real    0m9.832s
user    0m9.310s
sys 0m0.442s

info :

Apple LLVM version 9.0.0 (clang-900.0.39.2)
Target: x86_64-apple-darwin17.4.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

That's more than 2.5x faster !! Any thoughts ?

I replaced the __builtin_memcpy function by memcpy to test things out and this time the compiled code runs in about 34s on both sides - consistent and slower as expected.

It would appear that the combination of __builtin_memcpy and bitmasking is interpreted very differently by both compilers. I had a look at the assembly code, but couldn't see anything standing out that would explain such a drop in performance as I'm not an asm expert.


Edit 03-05-2018 : Posted this bug : https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84719.


like image 722
gpnuma Avatar asked Mar 04 '18 17:03

gpnuma


1 Answers

I find it suspicious that you get different code for memcpy vs __builtin_memcpy. I don't think that's supposed to happen, and indeed I cannot reproduce it on my (linux) system.

If you add #pragma GCC unroll 16 (implemented in gcc-8+) before the for loop, gcc gets the same perf as clang (making block a constant is essential to optimize the code), so essentially llvm's unrolling is more aggressive than gcc's, which can be good or bad depending on cases. Still, feel free to report it to gcc, maybe they'll tweak the unrolling heuristics some day and an extra testcase could help.

Once unrolling is taken care of, gcc does ok for some values (block equals 4 or 8 in particular), but much worse for some others, in particular 3. But that's better analyzed with a smaller testcase without the loop on block. Gcc seems to have trouble with memcpy(,,3), it works much better if you always read 8 bytes (the next line already takes care of the extra bytes IIUC). Another thing that could be reported to gcc.

like image 190
Marc Glisse Avatar answered Oct 24 '22 12:10

Marc Glisse