Por que a latência de gravação TCP é pior quando o trabalho é intercalado?

Eu tenho perfilado a latência TCP (em particular, owrite do espaço do usuário para o espaço do kernel de uma pequena mensagem), a fim de obter alguma intuição para a latência de umwrite (reconhecendo que isso pode ser específico do contexto). Percebi uma inconsistência substancial entre testes que, para mim, parece semelhante, e estou muito curioso para descobrir de onde vem a diferença. Entendo que as marcas de microbench podem ser problemáticas, mas ainda sinto que estou perdendo algum entendimento fundamental (já que as diferenças de latência são ~ 10x).

A configuração é que eu tenho um servidor TCP C ++ que aceita uma conexão de cliente (de outro processo na mesma CPU) e, ao conectar-me ao cliente, faz 20 chamadas de sistema parawrite ao soquete, enviando um byte de cada vez. O código completo do servidor é copiado no final desta postagem. Aqui está a saída que vezes cadawrite usandoboost/timer (que adiciona ruído de ~ 1 mic):

$ 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
...

Acho confiável que o primeirowrite é significativamente mais lento que os outros. Se eu embrulhar 10.000write chamadas em um timer, a média é de 2 microssegundos porwrite, mas a primeira chamada é sempre com mais de 15 microfones. Por que existe esse fenômeno de "aquecimento"?

De maneira semelhante, realizei um experimento em que entre cadawrite Eu faço um trabalho de bloqueio da CPU (calculando um grande número primo). Isso causatudo awrite chama a ser lento:

$ 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
...

Diante desses resultados, estou me perguntando se existe algum tipo de lote que ocorre durante o processo de cópia de bytes do buffer do usuário para o buffer do kernel. Se várioswrite as chamadas acontecem em rápida sucessão, elas se fundem em uma interrupção do kernel?

Em particular, estou procurando alguma noção de quanto tempowrite leva para copiar buffers do espaço do usuário para o espaço do kernel. Se houver algum efeito coalescente que permita que a médiawrite tomar apenas 2 microfones quando eu fizer 10.000 em sucessão, seria injustamente otimista concluir que owrite latência é 2 microfones; parece que minha intuição deve ser que cadawrite leva 20 microssegundos. Isso parece surpreendentemente lento para a menor latência possível (umawrite chamar um byte) sem o desvio do kernel.

Uma parte final dos dados é que, quando eu configuro um teste de ping-pong entre dois processos no meu computador (um servidor TCP e um cliente TCP), tenho em média 6 microfones por ida e volta (que inclui umread, umawrite, bem como mover-se pela rede localhost). Isso parece estar em desacordo com as latências de 20 mic para uma única gravação vista acima.

Código completo para o servidor TCP:

// 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]);

}

Código do cliente TCP:

// 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]);
    }
}

Eu tentei com e sem as bandeirasTCP_NODELAY, TCP_NOPUSH, SO_SNDBUF eSO_SNDLOWAT, com a ideia de que isso pode impedir o lote (mas meu entendimento é que esse lote ocorre entre o buffer do kernel e a rede, não entre o buffer do usuário e o buffer do kernel).

Aqui está o código do servidor para o teste de ping pong:

// 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);
}

Aqui está o código do cliente para o teste de ping pong:

// #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);
    }
}

questionAnswers(3)

yourAnswerToTheQuestion