calloc vs malloc+memset: Warum ist calloc langsamer?



  • Folgender Code:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int argc, char **argv)
    {
      int x, n, m;
      size_t antioptimize=0;
    
      if (argc != 4 ||
          sscanf(argv[1], "%d", &x) != 1 || x>1 || x<0 ||
          sscanf(argv[2], "%d", &n) != 1 || n<0 ||
          sscanf(argv[3], "%d", &m) != 1 || m<0) {
        printf("usage: %s <0 for malloc, 1 for calloc> <number of allocs> <bufsize>\n", argv[0]);
        return -1;
      }
    
      if (x==0) {
        puts("use malloc");
        while (n--) {
          char *buf = malloc(m);
          memset(buf, 0, m);
          antioptimize ^= (size_t)buf;
          free(buf);
        }
      } else {
        puts("use calloc");
        while (n--) {
          char *buf = calloc(1, m);
          antioptimize ^= (size_t)buf;
          free(buf);
        }
      }
      printf("antioptimize: %zu\n", antioptimize);
    
      return 0;
    }
    

    Kompiliert mit
    gcc -O1 -g -Wall -Wextra -std=c99 -pedantic calloc.c -o calloc

    Und den relevanten Assembler-Ausschnitten (generiert mit -S, aber auch in objdump vorhanden):

    .LBB2:
            .loc 1 21 0
            movslq  4(%rsp), %rdi
            call    malloc
    .LVL10:
            movq    %rax, %rbp
    .LVL11:
            .loc 1 22 0
            movslq  4(%rsp), %rdx
            movl    $0, %esi
            movq    %rax, %rdi
            call    memset
    .LVL12:
            .loc 1 23 0
            xorq    %rbp, %rbx
    .LVL13:
            .loc 1 24 0
            movq    %rbp, %rdi
            call    free
    

    und

    .L8:
    .LBB3:
            .loc 1 29 0
            movslq  4(%rsp), %rsi
         	movl    $1, %edi
            call    calloc
    .LVL18:
            .loc 1 30 0
         	xorq    %rax, %rbx
    .LVL19:
            .loc 1 31 0
            movq    %rax, %rdi
            call    free
    

    Hat folgende Performance:

    $ time ./calloc 0 100000000 8  
    use malloc
    antioptimize: 0
    ./calloc 0 100000000 8  1.97s user 0.00s system 99% cpu 1.969 total
    $ time ./calloc 0 100000000 512
    use malloc
    antioptimize: 0
    ./calloc 0 100000000 512  4.21s user 0.00s system 99% cpu 4.212 total
    $ time ./calloc 0 100000000 4096
    use malloc
    antioptimize: 0
    ./calloc 0 100000000 4096  10.94s user 0.00s system 99% cpu 10.953 total
    
    $ time ./calloc 1 100000000 8
    use calloc
    antioptimize: 0
    ./calloc 1 100000000 8  2.11s user 0.00s system 99% cpu 2.119 total
    $ time ./calloc 1 100000000 512
    use calloc
    antioptimize: 0
    ./calloc 1 100000000 512  4.51s user 0.00s system 99% cpu 4.511 total
    $ time ./calloc 1 100000000 4096
    use calloc
    antioptimize: 0
    ./calloc 1 100000000 4096  11.42s user 0.00s system 99% cpu 11.428 total
    

    Man sieht also, calloc ist konsistent langsamer als malloc+memset.

    Computer ist ein Standardlinux, ich denke nicht, dass es da grössere Unterschiede zwischen den Distros gibt. Würde mich aber auch interessieren, wie sich das auf Windows/Mac verhält.

    Hat jemand eine Erklärung dafür? calloc sollte doch eigentlich schneller sein, weil optimiert, aber doch sicher nicht langsamer.


  • Mod

    Ich kann deine Beobachtungen nicht nachvollziehen. Deine Zeiten sind auch sehr nahe beieinander, das könnten schon zufällige Varianzen sein. Hast du deine Messungen auch oft durchgeführt und den jeweils schnellsten Lauf verglichen?



  • Ich hab das schon mehrmals ausgeführt, aber nicht gepostet. Hier noch mehr Daten:

    $ for f in {0..20}; do time ./calloc 0 10000000 128 >/dev/null; done 
    ./calloc 0 10000000 128 > /dev/null  0.39s user 0.00s system 99% cpu 0.388 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.386 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.385 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.386 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.386 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.386 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.386 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.386 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.386 total
    ./calloc 0 10000000 128 > /dev/null  0.39s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.387 total
    ./calloc 0 10000000 128 > /dev/null  0.38s user 0.00s system 99% cpu 0.386 total
    $ for f in {0..20}; do time ./calloc 1 10000000 128 >/dev/null; done
    ./calloc 1 10000000 128 > /dev/null  0.42s user 0.00s system 99% cpu 0.417 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.416 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.415 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.416 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.415 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.416 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.415 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.415 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.413 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.413 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    ./calloc 1 10000000 128 > /dev/null  0.41s user 0.00s system 99% cpu 0.414 total
    

    Führe es aber ruhig bei dir aus, wenn du nicht glaubst.



  • Guck dir einfach mal die Implementierung von calloc an.


  • Mod

    callinrom schrieb:

    Führe es aber ruhig bei dir aus, wenn du nicht glaubst.

    Hab ich ja und wie gesagt, kann ich es nicht wirklich nachvollziehen. calloc könnte minimal komplexer implementiert sein, da calloc ein paar Tricks nutzen kann, wenn sehr große Speicherbereiche reserviert werden und entsprechend prüfen muss, ob dies der Fall ist. Vielleicht misst du diese Tests, die calloc macht. P.S.: Ich habe ein paar Hintergrundjobs auf meinem Testsystem übersehen. Wenn ich diese unterbreche, kann ich deine Beobachtungen nachvollziehen.

    Nur der Konsistenz wegen solltest du daher auch mal das Gegenteil testen: Wenige Allokationen, die dann aber auch richtig groß. Ruhig mal ein paar zig oder hundert Megabytes auf einmal. calloc sollte dann extrem flott werden, wohingegen malloc/memset in diesem Fall einbrechen sollte.



  • Hmmm, dann müsste das der Optimalfall für calloc sein:

    $ for f in {1..20}; do time ./calloc 0 1 $(bc <<< '4*(2^10)^3') >/dev/null; done
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.92s system 99% cpu 1.061 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.16s user 0.29s system 99% cpu 0.451 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.17s user 0.45s system 99% cpu 0.619 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.24s system 99% cpu 0.392 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.13s user 0.26s system 99% cpu 0.391 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.25s system 99% cpu 0.393 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.16s user 0.26s system 99% cpu 0.424 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.16s user 0.41s system 99% cpu 0.578 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.46s system 99% cpu 0.608 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.47s system 99% cpu 0.607 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.46s system 99% cpu 0.616 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 1.18s system 99% cpu 1.323 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.17s user 0.22s system 99% cpu 0.391 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.24s system 99% cpu 0.390 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.24s system 99% cpu 0.393 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.25s system 99% cpu 0.400 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.16s user 0.29s system 99% cpu 0.457 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.43s system 99% cpu 0.579 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.47s system 99% cpu 0.607 total
    ./calloc 0 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.47s system 99% cpu 0.611 total
    
    $ for f in {1..20}; do time ./calloc 1 1 $(bc <<< '4*(2^10)^3') >/dev/null; done
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 1.03s system 99% cpu 1.190 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.26s system 99% cpu 0.395 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.13s user 0.45s system 99% cpu 0.581 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.17s user 0.44s system 99% cpu 0.614 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.18s user 0.22s system 99% cpu 0.397 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.16s user 0.25s system 99% cpu 0.405 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.13s user 0.47s system 99% cpu 0.606 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 1.00s system 99% cpu 1.159 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.25s system 99% cpu 0.395 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.25s system 99% cpu 0.404 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.46s system 99% cpu 0.606 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.46s system 99% cpu 0.604 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.25s system 99% cpu 0.393 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.17s user 0.23s system 99% cpu 0.397 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.33s system 99% cpu 0.483 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 0.45s system 99% cpu 0.603 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.15s user 1.04s system 99% cpu 1.192 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.25s system 99% cpu 0.399 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.14s user 0.26s system 99% cpu 0.402 total
    ./calloc 1 1 $(bc <<< '4*(2^10)^3') > /dev/null  0.16s user 0.43s system 99% cpu 0.594 total
    

    Also selbst wenn calloc für diesen Fall optimiert wäre scheint es sich nicht zu lohnen. Die Zeiten sind sehr sprunghaft, aber die Topzeiten sind so ziemlich die gleichen.

    (musste mein Programm von int auf size_t umstellen um keinen Overflow zu kriegen.)

    hustbaer schrieb:

    Guck dir einfach mal die Implementierung von calloc an.

    Ok, hab ich gemacht, aber richtig schlau werde ich daraus nicht, insbesondere kann ich es nicht vergleichen, weil memset irgendwo anders versteckt ist (und scheinbar in Assembler implementiert ist).

    Da scheint schon Abfrage auf Betriebssystem-genullten Speicher zu sein, aber die letzte Messung scheint zu zeigen, dass das keinen messbaren Effekt hat.


  • Mod

    Du hast gerade gemessen, wie lange es dauert, wenn malloc/calloc fehlschlagen. 🙄

    Versuch mal 100x50000000. Da sollte sich ein deutlicher Unterschied zugunsten von calloc ergeben.



  • callinrom schrieb:

    ...und scheinbar in Assembler implementiert ist...

    waere das nicht eine gute antwort?

    da du ja die calloc implementierung siehst, kopier dir den memset bereich daraus und bastel damit deine eigene memset funktion die du dann mit malloc aufrufst anstatt der assembler version.



  • Das könnte auch der Overflow-check von calloc beim multiplizeren sein.


Anmelden zum Antworten