pac*_*tie 6 c sockets linux x86 linux-kernel
我的任务是开发一个测试软件,在一台 32GB RAM 的机器上的 Linux(X86-64,内核 4.15)上通过 1 个 TCP 套接字生成 100Gbps 的流量。
我开发了类似以下代码(为了简单起见,删除了一些健全性检查)来在一对 veth 接口(其中一个位于不同的 netns 中)上运行。
bmon根据开源软件,它在我的 PC 上生成大约 60Gbps 。令我惊讶的是,如果我删除该语句memset(buff, 0, size);,我会得到大约 94Gbps。这非常令人费解。
void test(int sock) {
int size = 500 * 0x100000;
char *buff = malloc(size);
//optional
memset(buff, 0, size);
int offset = 0;
int chunkSize = 0x200000;
while (1) {
offset = 0;
while (offset < size) {
chunkSize = size - offset;
if (chunkSize > CHUNK_SIZE) chunkSize = CHUNK_SIZE;
send(sock, &buff[offset], chunkSize, 0);
offset += chunkSize;
}
}
}
Run Code Online (Sandbox Code Playgroud)
我做了一些实验,用memset(buff, 0, size);以下内容替换(初始化一部分buff),
memset(buff, 0, size * ratio);
Run Code Online (Sandbox Code Playgroud)
如果比率为 0,则吞吐量最高,约为 94Gbps,当比率升至 100% (1.0) 时,吞吐量将下降至 60Gbps 左右。如果比率为 0.5 (50%),则吞吐量约为 72Gbps
感谢您对此提供的任何线索。
编辑 1 . 这是一个相对完整的代码,显示了在初始化缓冲区上进行复制的效果似乎更慢。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/stat.h>
int size = 500 * 0x100000;
char buf[0x200000];
double getTS() {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec + tv.tv_usec/1000000.0;
}
void test1(int init) {
char *p = malloc(size);
int offset = 0;
if (init) memset(p, 0, size);
double startTs = getTS();
for (int i = 0; i < 100; i++) {
offset = 0;
while (offset < size) {
memcpy(&buf[0], p+offset, 0x200000);
offset += 0x200000;
}
}
printf("took %f secs\n", getTS() - startTs);
}
int main(int argc, char *argv[]) {
test1(argc > 1);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
在我的电脑(Linux 18.04、Linux 4.15,32GB RAM)上,在没有初始化的情况下尝试了两次,花了 1.35 秒。初始化后,花费了 3.02 秒。
编辑 2 . 希望能够像从缓冲区发送全 0 一样快地获取 sendfile(感谢 @marco-bonelli)(通过 calloc)。我认为这很快就会成为我的任务的要求。
我一直在进行各种测试来调查这个令人惊讶的结果。
我编写了下面的测试程序,结合了初始化阶段和循环中的各种操作:
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/stat.h>
int alloc_size = 500 * 0x100000; // 500 MB
char copy_buf[0x200000]; // 2MB
double getTS() {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec + tv.tv_usec/1000000.0;
}
// set a word on each page of a memory area
void memtouch(void *buf, size_t size) {
uint64_t *p = buf;
size /= sizeof(*p);
for (size_t i = 0; i < size; i += 4096 / sizeof(*p))
p[i] = 0;
}
// compute the sum of words on a memory area
uint64_t sum64(const void *buf, size_t size) {
uint64_t sum = 0;
const uint64_t *p = buf;
size /= sizeof(*p);
for (size_t i = 0; i < size; i++)
sum += p[i];
return sum;
}
void test1(int n, int init, int run) {
int size = alloc_size;
char msg[80];
int pos = 0;
double times[n+1];
uint64_t sum = 0;
double initTS = getTS();
char *p = malloc(size);
pos = snprintf(msg + pos, sizeof msg - pos, "malloc");
if (init > 0) {
memset(p, init - 1, size);
pos += snprintf(msg + pos, sizeof msg - pos, "+memset%.0d", init - 1);
} else
if (init == -1) {
memtouch(p, size);
pos += snprintf(msg + pos, sizeof msg - pos, "+memtouch");
} else
if (init == -2) {
sum = sum64(p, size);
pos += snprintf(msg + pos, sizeof msg - pos, "+sum64");
} else {
/* leave p uninitialized */
}
pos += snprintf(msg + pos, sizeof msg - pos, "+rep(%d, ", n);
if (run > 0) {
pos += snprintf(msg + pos, sizeof msg - pos, "memset%.0d)", run - 1);
} else
if (run < 0) {
pos += snprintf(msg + pos, sizeof msg - pos, "sum64)");
} else {
pos += snprintf(msg + pos, sizeof msg - pos, "memcpy)");
}
double startTS = getTS();
for (int i = 0; i < n; i++) {
if (run > 0) {
memset(p, run - 1, size);
} else
if (run < 0) {
sum = sum64(p, size);
} else {
int offset = 0;
while (offset < size) {
memcpy(copy_buf, p + offset, 0x200000);
offset += 0x200000;
}
}
times[i] = getTS();
}
double firstTS = times[0] - startTS;
printf("%f + %f", startTS - initTS, firstTS);
if (n > 2) {
double avgTS = (times[n - 2] - times[0]) / (n - 2);
printf(" / %f", avgTS);
}
if (n > 1) {
double lastTS = times[n - 1] - times[n - 2];
printf(" / %f", lastTS);
}
printf(" secs %s", msg);
if (sum != 0) {
printf(" sum=%016llx", (unsigned long long)sum);
}
printf("\n");
free(p);
}
int main(int argc, char *argv[]) {
int n = 4;
if (argc < 2) {
test1(n, 0, 0);
test1(n, 0, 1);
test1(n, 0, -1);
test1(n, 1, 0);
test1(n, 1, 1);
test1(n, 1, -1);
test1(n, 2, 0);
test1(n, 2, 1);
test1(n, 2, -1);
test1(n, -1, 0);
test1(n, -1, 1);
test1(n, -1, -1);
test1(n, -2, 0);
test1(n, -2, 1);
test1(n, -2, -1);
} else {
test1(argc > 1 ? strtol(argv[1], NULL, 0) : n,
argc > 2 ? strtol(argv[2], NULL, 0) : 0,
argc > 3 ? strtol(argv[2], NULL, 0) : 0);
}
return 0;
}
Run Code Online (Sandbox Code Playgroud)
在运行 Debian Linux 3.16.0-11-amd64 的旧 Linux 机器上运行它,我得到了这些计时:
这些列是
0.000071 + 0.242601 / 0.113761 / 0.113711 secs malloc+rep(4, memcpy)
0.000032 + 0.349896 / 0.125809 / 0.125681 secs malloc+rep(4, memset)
0.000032 + 0.190461 / 0.049150 / 0.049210 secs malloc+rep(4, sum64)
0.350089 + 0.186691 / 0.186705 / 0.186548 secs malloc+memset+rep(4, memcpy)
0.350078 + 0.125603 / 0.125632 / 0.125531 secs malloc+memset+rep(4, memset)
0.349931 + 0.105991 / 0.105859 / 0.105788 secs malloc+memset+rep(4, sum64)
0.349860 + 0.186950 / 0.187031 / 0.186494 secs malloc+memset1+rep(4, memcpy)
0.349584 + 0.125537 / 0.125525 / 0.125535 secs malloc+memset1+rep(4, memset)
0.349620 + 0.106026 / 0.106114 / 0.105756 secs malloc+memset1+rep(4, sum64) sum=ebebebebebe80000
0.339846 + 0.186593 / 0.186686 / 0.186498 secs malloc+memtouch+rep(4, memcpy)
0.340156 + 0.125663 / 0.125716 / 0.125751 secs malloc+memtouch+rep(4, memset)
0.340141 + 0.105861 / 0.105806 / 0.105869 secs malloc+memtouch+rep(4, sum64)
0.190330 + 0.113774 / 0.113730 / 0.113754 secs malloc+sum64+rep(4, memcpy)
0.190149 + 0.400483 / 0.125638 / 0.125624 secs malloc+sum64+rep(4, memset)
0.190214 + 0.049136 / 0.049170 / 0.049149 secs malloc+sum64+rep(4, sum64)
Run Code Online (Sandbox Code Playgroud)
时间安排与 OP 的观察结果一致。我找到了一个与观察到的时间一致的解释:
如果对页的第一次访问是读操作,则时序明显好于第一次操作是写访问。
以下是与此解释一致的一些观察结果:
malloc()对于一个大的 500MB 块,只是进行系统调用来映射内存,它不会访问该内存,并且calloc可能会执行完全相同的操作。memset,对整个块的第一次访问是写访问,然后循环的时间会变慢1会产生完全相同的时序memtouch,仅将第一个单词写入零,我会在循环中得到相同的计时memcpy和sum64更快,因为第一次访问是读访问,而memset更慢,因为第一次访问是写访问。这似乎是 Linux 内核特有的,我在 macOS 上没有观察到相同的差异,但使用的是不同的处理器。此行为可能特定于较旧的 Linux 内核和/或 CPU 和桥架构。
最终更新:正如Peter Cordes所评论的那样,从从未写入的匿名页面进行读取将使每个页面的写时复制映射到相同的零物理页面,因此在读取时您可能会出现 TLB 未命中但 L1d 缓存命中的情况。(适用于.bss、 和来自 的内存mmap(MAP_ANONYMOUS),例如 glibccalloc并malloc用于大型分配。)他写了一些详细信息,并附perf有实验结果:Why is iterating through `std::vector`比迭代`std::array`更快?
这解释了为什么memcpy从仅隐式初始化为零的内存中读取比从显式写入的内存中读取要快。对于稀疏数据使用+calloc()代替的一个很好的理由。malloc()memset()
| 归档时间: |
|
| 查看次数: |
355 次 |
| 最近记录: |