web-dev-qa-db-de.com

Warum ist TCP Schreibverzögerung schlechter, wenn die Arbeit verschachtelt ist?

Ich habe die TCP -Latenz (insbesondere die write vom Benutzerraum zum Kernelraum einer kleinen Nachricht) profiliert, um mir ein Bild von der Latenz einer write zu machen (wobei ich bestätige, dass dies kontextspezifisch sein kann) ). Ich habe erhebliche Inkonsistenzen zwischen Tests festgestellt, die mir ähnlich erscheinen, und bin sehr gespannt, woher der Unterschied stammt. Ich verstehe, dass Mikrobenchmarks problematisch sein können, aber ich habe immer noch das Gefühl, dass mir ein grundlegendes Verständnis fehlt (da die Latenzunterschiede ~ 10x betragen).

Das Setup ist, dass ich einen C++ TCP Server habe, der eine Client-Verbindung (von einem anderen Prozess auf derselben CPU) akzeptiert und beim Herstellen einer Verbindung mit dem Client 20 Systemaufrufe an write an den Socket durchführt und einen sendet Byte auf einmal. Der vollständige Code des Servers wird am Ende dieses Beitrags kopiert. Hier ist die Ausgabe, bei der jeweils writeboost/timer verwendet wird (wodurch Rauschen von ~ 1 Mikrofon hinzugefügt wird):

$ clang++ -std=c++11 -stdlib=libc++ tcpServerStove.cpp -O3; ./a.out
18 mics
3 mics
3 mics
4 mics
3 mics
3 mics
4 mics
3 mics
5 mics
3 mics
...

Ich finde zuverlässig, dass das erste write deutlich langsamer ist als die anderen. Wenn ich 10.000 write-Anrufe in einen Timer einwickle, beträgt der Durchschnitt 2 Mikrosekunden pro write, aber der erste Anruf ist immer 15+ Mikrofone. Warum gibt es dieses "Aufwärm" -Phänomen?

Entsprechend habe ich ein Experiment durchgeführt, bei dem ich zwischen jedem Aufruf von write einige blockierende CPU-Aufgaben erledige (Berechnung einer großen Primzahl). Dies führt dazu, dass all die write-Aufrufe langsam sind:

$ clang++ -std=c++11 -stdlib=libc++ tcpServerStove.cpp -O3; ./a.out
20 mics
23 mics
23 mics
30 mics
23 mics
21 mics
21 mics
22 mics
22 mics
...

Angesichts dieser Ergebnisse frage ich mich, ob es eine Art Stapelverarbeitung gibt, die während des Kopiervorgangs von Bytes aus dem Benutzerpuffer in den Kernelpuffer stattfindet. Wenn mehrere write-Aufrufe schnell hintereinander ausgeführt werden, werden sie dann zu einem Kernel-Interrupt zusammengefasst?

Insbesondere suche ich nach einer Vorstellung davon, wie lange write dauert, um Puffer vom User-Space in den Kernel-Space zu kopieren. Wenn es einen Verschmelzungseffekt gibt, der es dem durchschnittlichen write erlaubt, nur 2 Mikrofone aufzunehmen, wenn ich 10.000 nacheinander mache, dann wäre es unfair optimistisch zu folgern, dass die write-Latenz 2 Mikrofone beträgt; es scheint, dass meine Intuition sein sollte, dass jedes write 20 Mikrosekunden dauert. Dies scheint überraschend langsam zu sein, da die niedrigste Latenz (ein unformatierter Aufruf von write für ein Byte) ohne Kernel-Bypass erreicht werden kann.

Ein letztes Datenelement ist, dass ich beim Einrichten eines Ping-Pong-Tests zwischen zwei Prozessen auf meinem Computer (einem TCP Server und einem TCP Client) durchschnittlich 6 Mikrofone pro Runde habe Reise (die ein read, ein write sowie das Bewegen durch das Localhost-Netzwerk umfasst). Dies scheint im Widerspruch zu den 20 Mic-Latenzen für einen einzelnen Schreibvorgang zu stehen.

Vollständiger Code für den TCP Server:

// Server side C/C++ program to demonstrate Socket programming
// #include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <boost/timer.hpp>
#include <unistd.h>

// Set up some blocking work.
bool isPrime(int n) {
    if (n < 2) {
        return false;
    }

    for (int i = 2; i < n; i++) {
        if (n % i == 0) {
            return false;
        }
    }

    return true;
}

// Compute the nth largest prime. Takes ~1 sec for n = 10,000
int getPrime(int n) {
    int numPrimes = 0;
    int i = 0;
    while (true) {
        if (isPrime(i)) {
            numPrimes++;
            if (numPrimes >= n) {
                return i;
            }
        }
        i++;
    }
}

int main(int argc, char const *argv[])
{
    int server_fd, new_socket, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // Create socket for TCP server
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    // Prevent writes from being batched
    setsockopt(server_fd, SOL_SOCKET, TCP_NODELAY, &opt, sizeof(opt));
    setsockopt(server_fd, SOL_SOCKET, TCP_NOPUSH, &opt, sizeof(opt));
    setsockopt(server_fd, SOL_SOCKET, SO_SNDBUF, &opt, sizeof(opt));
    setsockopt(server_fd, SOL_SOCKET, SO_SNDLOWAT, &opt, sizeof(opt));

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    bind(server_fd, (struct sockaddr *)&address, sizeof(address));

    listen(server_fd, 3);

    // Accept one client connection
    new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);

    char sendBuffer[1] = {0};
    int primes[20] = {0};
    // Make 20 sequential writes to kernel buffer.
    for (int i = 0; i < 20; i++) {
        sendBuffer[0] = i;
        boost::timer t;
        write(new_socket, sendBuffer, 1);
        printf("%d mics\n", int(1e6 * t.elapsed()));

        // For some reason, doing some blocking work between the writes
        // The following work slows down the writes by a factor of 10.
        // primes[i] = getPrime(10000 + i);
    }

    // Print a prime to make sure the compiler doesn't optimize
    // away the computations.
    printf("prime: %d\n", primes[8]);

}

TCP-Client-Code:

// Server side C/C++ program to demonstrate Socket programming
// #include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    int sock, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // We'll be passing uint32's back and forth
    unsigned char recv_buffer[1024] = {0};

    // Create socket for TCP server
    sock = socket(AF_INET, SOCK_STREAM, 0);

    setsockopt(sock, SOL_SOCKET, TCP_NODELAY, &opt, sizeof(opt));

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    // Accept one client connection
    if (connect(sock, (struct sockaddr *)&address, (socklen_t)addrlen) != 0) {
        throw("connect failed");
    }

    read(sock, buffer_pointer, num_left);

    for (int i = 0; i < 10; i++) {
        printf("%d\n", recv_buffer[i]);
    }
}

Ich habe mit und ohne die Flags TCP_NODELAY, TCP_NOPUSH, SO_SNDBUF und SO_SNDLOWAT versucht, mit der Idee, dass dies das Batching verhindern könnte (aber ich verstehe, dass dieses Batching zwischen dem Kernel-Puffer und dem Netzwerk stattfindet, nicht zwischen dem Benutzer-Puffer und dem Kernel-Puffer). .

Hier ist der Servercode für den Ping-Pong-Test:

// Server side C/C++ program to demonstrate Socket programming
// #include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <boost/timer.hpp>
#include <unistd.h>

 __inline__ uint64_t rdtsc(void)
   {
uint32_t lo, hi;
__asm__ __volatile__ (
        "xorl %%eax,%%eax \n        cpuid"
        ::: "%rax", "%rbx", "%rcx", "%rdx");
__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
return (uint64_t)hi << 32 | lo;
 }

// Big Endian (network order)
unsigned int fromBytes(unsigned char b[4]) {
    return b[3] | b[2]<<8 | b[1]<<16 | b[0]<<24;
}

void toBytes(unsigned int x, unsigned char (&b)[4]) {
    b[3] = x;
    b[2] = x>>8;
    b[1] = x>>16;
    b[0] = x>>24;
}

int main(int argc, char const *argv[])
{
    int server_fd, new_socket, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    unsigned char recv_buffer[4] = {0};
    unsigned char send_buffer[4] = {0};

    // Create socket for TCP server
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    bind(server_fd, (struct sockaddr *)&address, sizeof(address));

    listen(server_fd, 3);

    // Accept one client connection
    new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
    printf("Connected with client!\n");

    int counter = 0;
    unsigned int x = 0;
    auto start = rdtsc();
    boost::timer t;

    int n = 10000;
    while (counter < n) {
        valread = read(new_socket, recv_buffer, 4);
        x = fromBytes(recv_buffer);
        toBytes(x+1, send_buffer);
        write(new_socket, send_buffer, 4);
        ++counter;
    }

    printf("%f clock cycles per round trip (rdtsc)\n",  (rdtsc() - start) / double(n));
    printf("%f mics per round trip (boost timer)\n", 1e6 * t.elapsed() / n);
}

Hier ist der Client-Code für den Ping-Pong-Test:

// #include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <boost/timer.hpp>
#include <unistd.h>

// Big Endian (network order)
unsigned int fromBytes(unsigned char b[4]) {
    return b[3] | b[2]<<8 | b[1]<<16 | b[0]<<24;
}

void toBytes(unsigned int x, unsigned char (&b)[4]) {
    b[3] = x;
    b[2] = x>>8;
    b[1] = x>>16;
    b[0] = x>>24;
}

int main(int argc, char const *argv[])
{
    int sock, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // We'll be passing uint32's back and forth
    unsigned char recv_buffer[4] = {0};
    unsigned char send_buffer[4] = {0};

    // Create socket for TCP server
    sock = socket(AF_INET, SOCK_STREAM, 0);

    // Set TCP_NODELAY so that writes won't be batched
    setsockopt(sock, SOL_SOCKET, TCP_NODELAY, &opt, sizeof(opt));

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    // Accept one client connection
    if (connect(sock, (struct sockaddr *)&address, (socklen_t)addrlen) != 0) {
        throw("connect failed");
    }

    unsigned int lastReceived = 0;
    while (true) {
        toBytes(++lastReceived, send_buffer);
        write(sock, send_buffer, 4);
        valread = read(sock, recv_buffer, 4);
        lastReceived = fromBytes(recv_buffer);
    }
}
14
rampatowl

Hier gibt es einige Probleme.

Um der Antwort näher zu kommen, müssen Sie zwei Dinge von Ihrer Kundenseite erledigen lassen: 1. Alle Daten erhalten. 2. Verfolgen Sie, wie groß jeder Lesevorgang war. Ich habe das gemacht durch:

  int loc[N+1];
int nloc, curloc;
for (nloc = curloc = 0; curloc < N; nloc++) {
    int n = read(sock, recv_buffer + curloc, sizeof recv_buffer-curloc);
    if (n <= 0) {
            break;
    }
    curloc += n;
    loc[nloc] = curloc;
}
int last = 0;
for (int i = 0; i < nloc; i++) {
    printf("%*.*s ", loc[i] - last, loc[i] - last, recv_buffer + last);
    last = loc[i];
}
printf("\n");

und Definieren von N bis 20 (Entschuldigung, Erziehung) und Ändern Ihres Servers, um a-z Byte für Byte zu schreiben. Wenn nun etwas ausgedruckt wird:

 a b c d e f g h i j k l m n o p q r s 

wir wissen, dass der Server 1-Byte-Pakete sendet. Wenn jedoch etwas gedruckt wird:

 a bcde fghi jklm nop qrs 

wir vermuten, dass der Server hauptsächlich 4-Byte-Pakete sendet.

Das Hauptproblem ist, dass TCP_NODELAY nicht das tut, was Sie vermuten. Der Nagle-Algorithmus sammelt die Ausgabe, wenn ein nicht bestätigtes gesendetes Paket vorliegt. TCP_NODELAY steuert, ob dies angewendet wird.

Unabhängig von TCP_NODELAY sind Sie immer noch ein STREAM_SOCKET, was bedeutet, dass N-Schreibvorgänge zu einem zusammengefasst werden können. Die Steckdose speist das Gerät, gleichzeitig speisen Sie die Steckdose. Nachdem ein Paket [mbuf, skbuff, ...] an das Gerät übergeben wurde, muss der Socket ein neues Paket für die nächsten write () erstellen. Sobald das Gerät für ein neues Paket bereit ist, kann es vom Socket bereitgestellt werden. Bis dahin dient das Paket als Puffer. Im Puffermodus ist das Schreiben sehr schnell, da alle erforderlichen Datenstrukturen verfügbar sind (wie in Kommentaren und anderen Antworten erwähnt).

Sie können diese Pufferung steuern, indem Sie die Socketoptionen SO_SNDBUF und SO_SNDLOWAT anpassen. Beachten Sie jedoch, dass der von Accept zurückgegebene Puffer die Puffergrößen des bereitgestellten Sockets nicht erbt. Durch Reduzieren der SNDBUF auf 1

Die Ausgabe unten:

abcdefghijklmnopqrst 
a bcdefgh ijkl mno pqrst 
a b cdefg hij klm nop qrst 
a b c d e f g h i j k l m n o p q r s t 

entspricht dem Standardwert und fügt dann bei nachfolgenden Verbindungen nacheinander Folgendes hinzu: TCP_NODELAY, TCP_NOPUSH, SO_SNDBUF (= 1), SO_SNDLOWAT (= 1). Jede Iteration hat ein flacheres Zeit-Delta als die vorherige.

Ihre Laufleistung wird wahrscheinlich variieren, dies war auf MacOS 10.12; und ich habe Ihre Programme mit rdtsc () in C++ geändert, weil ich Vertrauensprobleme habe.

/* srv.c */
// Server side C/C++ program to demonstrate Socket programming
// #include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdbool.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <unistd.h>

#ifndef N
#define N 20
#endif
int nap = 0;
int step = 0;
extern long rdtsc(void);

void xerror(char *f) {
    perror(f);
    exit(1);
}
#define Z(x)   if ((x) == -1) { xerror(#x); }

void sopt(int fd, int opt, int val) {
    Z(setsockopt(fd, SOL_SOCKET, opt, &val, sizeof(val)));
}
int gopt(int fd, int opt) {
    int val;
    socklen_t r = sizeof(val);
    Z(getsockopt(fd, SOL_SOCKET, opt, &val, &r));
    return val;
}

#define POPT(fd, x)  printf("%s %d ", #x, gopt(fd, x))
void popts(char *tag, int fd) {
    printf("%s: ", tag);
    POPT(fd, SO_SNDBUF);
    POPT(fd, SO_SNDLOWAT);
    POPT(fd, TCP_NODELAY);
    POPT(fd, TCP_NOPUSH);
    printf("\n");
}

void stepsock(int fd) {
     switch (step++) {
     case 7:
    step = 2;
     case 6:
         sopt(fd, SO_SNDLOWAT, 1);
     case 5:
         sopt(fd, SO_SNDBUF, 1);
     case 4:
         sopt(fd, TCP_NOPUSH, 1);
     case 3:
         sopt(fd, TCP_NODELAY, 1);
     case 2:
     break;
     }
}

int main(int argc, char const *argv[])
{
    int server_fd, new_socket, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);



    // Create socket for TCP server
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    popts("original", server_fd);
    // Set TCP_NODELAY so that writes won't be batched
    while ((opt = getopt(argc, argv, "sn:o:")) != -1) {
    switch (opt) {
    case 's': step = ! step; break;
    case 'n': nap = strtol(optarg, NULL, 0); break;
    case 'o':
        for (int i = 0; optarg[i]; i++) {
            switch (optarg[i]) {
            case 't': sopt(server_fd, TCP_NODELAY, 1); break;
            case 'p': sopt(server_fd, TCP_NOPUSH, 0); break;
            case 's': sopt(server_fd, SO_SNDBUF, 1); break;
            case 'l': sopt(server_fd, SO_SNDLOWAT, 1); break;
            default:
                exit(1);
            }
        }
    }
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) == -1) {
    xerror("bind");
    }
    popts("ready", server_fd);
    while (1) {
        if (listen(server_fd, 3) == -1) {
        xerror("listen");
        }

        // Accept one client connection
        new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
        if (new_socket == -1) {
        xerror("accept");
        }
            popts("accepted: ", new_socket);
        sopt(new_socket, SO_SNDBUF, gopt(server_fd, SO_SNDBUF));
        sopt(new_socket, SO_SNDLOWAT, gopt(server_fd, SO_SNDLOWAT));
        if (step) {
                stepsock(new_socket);
            }
        long tick[21];
        tick[0] = rdtsc();
        // Make N sequential writes to kernel buffer.
        for (int i = 0; i < N; i++) {
                char ch = 'a' + i;

        write(new_socket, &ch, 1);
        tick[i+1] = rdtsc();

        // For some reason, doing some blocking work between the writes
        // The following work slows down the writes by a factor of 10.
        if (nap) {
           sleep(nap);
        }
        }
        for (int i = 1; i < N+1; i++) {
        printf("%ld\n", tick[i] - tick[i-1]);
        }
        printf("_\n");

        // Print a prime to make sure the compiler doesn't optimize
        // away the computations.
        close(new_socket);
    }
}

clnt.c:

#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <unistd.h>

#ifndef N
#define N 20
#endif
int nap = 0;

int main(int argc, char const *argv[])
{
    int sock, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // We'll be passing uint32's back and forth
    unsigned char recv_buffer[1024] = {0};

    // Create socket for TCP server
    sock = socket(AF_INET, SOCK_STREAM, 0);

    // Set TCP_NODELAY so that writes won't be batched
    setsockopt(sock, SOL_SOCKET, TCP_NODELAY, &opt, sizeof(opt));

    while ((opt = getopt(argc,argv,"n:")) != -1) {
        switch (opt) {
        case 'n': nap = strtol(optarg, NULL, 0); break;
        default:
            exit(1);
        }
    }
    opt = 1;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    // Accept one client connection
    if (connect(sock, (struct sockaddr *)&address, (socklen_t)addrlen) != 0) {
        perror("connect failed");
    exit(1);
    }
    if (nap) {
    sleep(nap);
    }
    int loc[N+1];
    int nloc, curloc; 
    for (nloc = curloc = 0; curloc < N; nloc++) {
    int n = read(sock, recv_buffer + curloc, sizeof recv_buffer-curloc);
        if (n <= 0) {
        perror("read");
        break;
    }
    curloc += n;
    loc[nloc] = curloc;
    }
    int last = 0;
    for (int i = 0; i < nloc; i++) {
    int t = loc[i] - last;
    printf("%*.*s ", t, t, recv_buffer + last);
    last = loc[i];
    }
    printf("\n");
    return 0;
}

rdtsc.s:

.globl _rdtsc
_rdtsc:
    rdtsc
    shl $32, %rdx
    or  %rdx,%rax
    ret
3
mevets

(Nicht ganz eine Antwort, brauchte aber etwas mehr Platz als ein Kommentar ...)

Das klingt wie Nagles Algorithmus oder eine Variante davon und steuert, wann TCP Pakete tatsächlich gesendet werden.

Wenn beim ersten Schreiben keine unbestätigten Daten in der "Pipe" vorhanden sind, werden diese sofort gesendet, was einen Moment dauert. Für nachfolgende Schreibvorgänge in Kürze befinden sich noch unbestätigte Daten in der Pipe, sodass eine kleine Datenmenge im Sendepuffer in die Warteschlange gestellt werden kann, was schneller ist.

Nach einer Unterbrechung der Übertragung, wenn alle Sends eine Chance zum Aufholen hatten, kann die Pipe sofort wieder gesendet werden.

Sie können dies mit etwas wie Wireshark überprüfen, um die tatsächlichen TCP -Pakete anzuzeigen - dies zeigt, wie die write()-Anforderungen gruppiert werden.

Um fair zu sein, würde ich erwarten, dass das TCP_NODELAY-Flag dies umgeht. Dies führt zu einer gleichmäßigeren Verteilung der Timings, wie Sie sagen. Wenn Sie die TCP -Pakete überprüfen können, lohnt es sich auch, zu prüfen, ob das PSH-Flag gesetzt ist, um ein sofortiges Senden zu erzwingen.

4
df778899

(Nicht sicher, ob dies hilfreich sein kann, aber ich habe nicht genug Ansehen, um einen Kommentar zu posten.)

Microbenchmarking ist insbesondere bei OS-Anrufen knifflig - meiner Erfahrung nach müssen nur wenige Faktoren berücksichtigt und herausgefiltert oder gemessen werden, bevor Zahlen endgültig erfasst werden. 

Einige dieser Faktoren sind:

  1. cache-Hits/Misses

  2. multitasking-Vorkaufsrecht

  3. Betriebssystemzuweisung von Speicher zu bestimmten Zeitpunkten von API-Aufrufen (Speicherzuordnung kann leicht zu Verzögerungen von Mikrosekunden führen)

  4. lazy Loading (einige APIs können während connect-Aufruf von fe. nicht viel tun, bis echte Daten eingehen)

  5. aktuelle Taktrate der CPU im Moment (dynamische Taktskalierung, geschieht ständig)

  6. kürzlich ausgeführte Befehle für diesen oder benachbarte Kerne (z. B. können schwere AVX512-Anweisungen die CPU in den L2-Modus (Lizenz 2) umschalten, wodurch der Takt verlangsamt wird, um eine Überhitzung zu vermeiden).

  7. bei der Virtualisierung läuft alles, was sonst auf derselben physischen CPU läuft.

Sie können versuchen, den Einfluss der Faktoren 1, 2, 6 und 7 abzuschwächen, indem Sie denselben Befehl wiederholt in einem Zyklus ausführen. In Ihrem Fall kann es jedoch bedeuten, dass Sie mehrere Sockets gleichzeitig öffnen und den ersten Schreibvorgang für jeden von ihnen in einem Zyklus messen müssen. Auf diese Weise wird der Cache für den Kernel beim ersten Aufruf vorgewärmt, und weitere Aufrufe haben eine "sauberere" Zeit. Sie können es ausrechnen.

Um mit 5 zu helfen, können Sie versuchen, den CPU-Takt "vorzuwärmen" - einen langen Blockierungszyklus direkt vor Ihrem Test und innerhalb Ihrer Testschleife auszuführen, aber machen Sie in diesem Zyklus nichts Besonderes, um Überhitzung zu vermeiden. Am sichersten ist es, __asm("nop") aufzurufen. innerhalb dieses Zyklus. 

Zuerst habe ich nicht bemerkt, dass Sie nur 1 Byte senden, und dachte, dass dies an TCP Slow Start liegen könnte. Aber auch Ihr 2. Test mit Primzahl unterstützt dies nicht. Es klingt also eher nach den Faktoren 1, 5 oder 6 aus meiner Liste.

1
john316