【问题标题】:Why is TCP write latency worse when work is interleaved?为什么交错工作时 TCP 写入延迟更差?
【发布时间】:2019-02-08 17:14:19
【问题描述】:

我一直在分析 TCP 延迟(特别是小消息从用户空间到内核空间的write),以便对write 的延迟有一些直觉(承认这可能是上下文-具体的)。我注意到在我看来相似的测试之间存在很大的不一致,我很想知道差异来自哪里。我知道微基准测试可能会出现问题,但我仍然觉得我缺少一些基本的理解(因为延迟差异约为 10 倍)。

设置是我有一个 C++ TCP 服务器,它接受一个客户端连接(来自同一 CPU 上的另一个进程),并且在与客户端连接时,对套接字进行 20 次系统调用 write,发送一个字节一次。服务器的完整代码复制在本文末尾。这是使用boost/timer 对每个write 计时的输出(这会增加约1 个麦克风的噪音):

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

我确实发现第一个write 比其他的慢得多。如果我将 10,000 个 write 呼叫包装在一个计时器中,则平均每个 write 需要 2 微秒,但第一个呼叫始终是 15 个以上的麦克风。为什么会出现这种“升温”现象?

与此相关,我进行了一个实验,在每个 write 调用之间,我做了一些阻塞 CPU 工作(计算一个大素数)。这会导致 所有write 调用变慢:

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

鉴于这些结果,我想知道在将字节从用户缓冲区复制到内核缓冲区的过程中是否发生了某种批处理。如果多个write 调用快速连续发生,它们是否会合并为一个内核中断?

我特别想知道write 将缓冲区从用户空间复制到内核空间需要多长时间。如果在我连续 10,000 次时,有某种合并效应允许平均 write 只占用 2 个麦克风,那么得出 write 延迟为 2 个麦克风的结论是不公平的乐观;看来我的直觉应该是每个write 需要20 微秒。对于您可以获得的最低延迟(一个字节上的原始write 调用),这似乎非常慢。

最后一条数据是,当我在我的计算机上的两个进程(一个 TCP 服务器和一个 TCP 客户端)之间设置一个乒乓测试时,我平均每次往返 6 个麦克风(其中包括一个readwrite,以及通过 localhost 网络移动)。这似乎与上面看到的单次写入的 20 个麦克风延迟不一致。

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

}

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

我尝试使用和不使用标志 TCP_NODELAYTCP_NOPUSHSO_SNDBUFSO_SNDLOWAT,并认为这可能会阻止批处理(但我的理解是这种批处理发生在内核缓冲区和网络之间,而不是在用户缓冲区和内核缓冲区之间)。

这是乒乓球测试的服务器代码:

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

这是乒乓测试的客户端代码:

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

【问题讨论】:

  • 旁注: 在 linux 下,您可以使用 PACKET_MMAP 将内核缓冲区直接映射到用户空间内存。请参阅:kernel.org/doc/Documentation/networking/packet_mmap.txt IIRC,缺点是您必须自己处理数据包级别(即PF_PACKET
  • 您可以将时间与写入/dev/null、管道、ramdisk 上的文件等进行比较,以查看用户空间到内核的复制需要多长时间(应该大致相当于@ 987654355@)
  • 嘿克雷格,感谢您的提示。我对 PACKET_MMAP 的理解是否正确,它处于原始数据包的级别,所以我需要在它之上实现 TCP? (例如重新排序消息、检查数据丢失、解码标头等?)
  • 是的。您将获得以太网标头、IP 标头,然后是 TCP 和/或 UDP 标头 [取决于],您必须解码/剥离它们。这不是太难。我最近不得不在商业产品中这样做,因为发送者的低级硬件只是盲目地爆出数据包,所以我不得不这样做。为了提高速度,我准备做PACKET_MMAP,但是在基准测试之后,使用recv就足够了。仅供参考,PF_PACKETtcpdumplibpcap 使用的。见:blog.cloudflare.com/kernel-bypassstackoverflow.com/questions/18343365/…
  • 你有 Spectre/Meltdown 更新吗?如果您的主要测试运行时间足够长以进行上下文切换,您可能会在代码和缓冲区上获得额外的 TLB 和缓存刷新。

标签: c performance tcp linux-kernel


【解决方案1】:

(不是一个完整的答案,但需要比评论更多的空间......)

这听起来确实像 Nagle's algorithm 或其变体,控制 TCP 数据包的实际发送时间。

第一次写入,当'管道'中没有未确认的数据时,会立即发送,需要一点时间。对于之后不久的后续写入,管道中仍然会有未确认的数据,因此可以在发送缓冲区中排队少量数据,这样更快。

在传输中断后,当所有发送都有机会赶上时,管道将立即准备再次发送。

您可以使用 Wireshark 之类的工具查看实际的 TCP 数据包来确认这一点 - 这将显示 write() 请求是如何组合在一起的。

公平地说,我希望 TCP_NODELAY 标志能够绕过这一点——正如你所说,导致时间分布更均匀。如果您可以检查 TCP 数据包,还值得查看它们是否显示了 PSH 标志集,以强制立即发送。

【讨论】:

  • 您好,谢谢您的回复!我还没有设置wireshark实验,但似乎你的假设独立写入总是很慢(~20个麦克风)被乒乓测试的低延迟排除了,每次后续写入都需要首先读取新数据(这意味着客户端收到了之前的写入)。由于这里的往返时间是 6 个麦克风,因此独立写入延迟似乎不可能是 20 个麦克风。
【解决方案2】:

这里有一些问题。

为了更接近答案,您需要让客户端做两件事:1. 接收所有数据。 2. 跟踪每次读取的大小。我是这样做的:

  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");

并将 N 定义为 20(抱歉,成长),并将您的服务器更改为一次写入 a-z 一个字节。现在,当打印出如下内容时:

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

我们知道服务器正在发送 1 个字节的数据包;但是当它打印出类似的东西时:

 a bcde fghi jklm nop qrs 

我们怀疑服务器主要发送 4 字节数据包。

根本问题是 TCP_NODELAY 没有做你怀疑的事情。 Nagle 算法,当有未确认的发送数据包时累积输出; TCP_NODELAY 控制是否应用。

不管TCP_NODELAY,你仍然是一个STREAM_SOCKET,这意味着N-writes可以合并为一个。插座正在为设备供电,但同时您也在为插座供电。一旦数据包 [mbuf, skbuff, ...] 已提交给设备,套接字需要在下一次 write() 时创建一个新数据包。一旦设备准备好接收新数据包,套接字就可以提供它,但在那之前,数据包将用作缓冲区。在缓冲模式下,写入速度非常快,因为所有必要的数据结构都可用[如 cmets 和其他答案中所述]。

您可以通过调整 SO_SNDBUF 和 SO_SNDLOWAT 套接字选项来控制此缓冲。请注意,accept 返回的缓冲区不会继承所提供套接字的缓冲区大小。通过将 SNDBUF 减少到 1

下面的输出:

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 

corresponds 从默认开始,然后在后续连接中依次向服务器端添加:TCP_NODELAY、TCP_NOPUSH、SO_SNDBUF (=1)、SO_SNDLOWAT (=1)。每次迭代的时间增量都比前一次更平坦。

您的里程可能会有所不同,这是在 MacOS 10.12 上;我用 rdtsc() 把你的程序改成了 C++ 的东西,因为我有信任问题。

/* 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

【讨论】:

  • 谢谢,这非常有用。不过,我仍然对这与原始素数与非素数测试有何关系感到困惑。您说素数计算使事情变得模糊,因为时间计算现在包括 writegetPrime 调用,但事实并非如此; getPrime 不在通话时间。我只是用 SO_SNDBUF 等重新计算了时间,没有getPrime 调用,事情仍然更快。也许我错过了你的意思?
  • 抱歉,我用 rdtsc() 替换了 boost timer,它确实占用了整个循环,然后将我看到的不可靠结果归咎于 boost timer 有同样的问题。我认为增强计时器为网卡提供了足够的时间来清除挂起的缓冲区,因此每个字符都放在一个单独的数据包中。如果你输入我的接收代码,你应该可以看到这个。
  • 我为 rdtsc() 换掉了升压计时器,并包含了 SO_SNDBUF 等,但是当我随后将 getPrime 调用添加 10 倍时,我的 writes 仍然明显变慢。澄清一下,你看到了什么不同的东西吗?如果是这样,请粘贴您的精确服务器代码,以便我尝试复制。
  • 如果你用-s运行服务器,每个客户端连接都会在stepsock()中单步执行sockopts。
  • 你的意思是像g++ -s -std=c++11 -stdlib=libc++ tcpServerStove.cpp -O3; ./a.out这样的东西吗?我收到ld: warning: option -s is obsolete and being ignored。顺便说一句,感谢您的耐心等待,我对这些东西还很陌生!
【解决方案3】:

(不确定这是否有帮助,但我没有足够的声誉来发表评论)

微基准测试很棘手,尤其是对于操作系统调用 - 根据我的经验,在得出结论之前,必须考虑和过滤或测量一些因素。

其中一些因素是:

  1. 缓存命中/未命中

  2. 多任务抢占

  3. 操作系统在 API 调用的某些时刻分配内存(内存分配很容易导致微秒级的延迟)

  4. 延迟加载(某些 API 在connect 调用 f.e. 期间可能不会做太多事情,直到真正的数据进来)

  5. 当前 CPU 的实际时钟速度(动态时钟缩放,一直发生)

  6. 最近在此内核或相邻内核上执行的命令(例如,繁重的 AVX512 指令可能会将 CPU 切换到 L2(许可证 2)模式,这会降低时钟速度以避免过热)。

  7. 有了虚拟化,任何其他东西都可以在同一个物理 CPU 上运行。

您可以尝试通过循环重复运行相同的命令来减轻因素 1、2、6 和 7 的影响。但是,在您的情况下,这可能意味着您需要一次打开多个套接字并在一个周期内测量每个套接字的第一次写入。这样一来,您进入内核的缓存将在第一次调用时被预热,并且进一步的调用将有“更干净”的时间。你可以平均一下。

为了帮助解决 5,您可以尝试“预热” CPU 时钟 - 在测试之前和测试循环内运行一个较长的阻塞周期,但不要在该周期内做任何花哨的事情以避免过热 - 最安全的是在该周期内调用__asm("nop")

一开始没注意到你只发送了1个字节,以为这可能是TCP slow start的原因。但是您的第二次质数测试也不支持这一点。所以,这听起来更像是我列表中的因素 1、5 或 6。

【讨论】:

  • 你能详细解释一下#5吗?按照您的建议预热缓存然后在紧密循环中运行写入对我来说不是一个非常有用的测试,因为我试图在实际条件下测试write 延迟,其中一些工作将在每个之间交错写。
  • 我并不是建议在循环中运行多次写入会帮助您在现实生活中 - 我只是建议使用这种方法在您的测量中过滤缓存未命中的影响,看看是否还有其他东西除了使第一次写入变慢的缓存(或在您的测试中使用质数连续写入)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-05
  • 2013-08-29
  • 1970-01-01
相关资源
最近更新 更多