Я профилировал задержку TCP (в частности, write
из пространства пользователя в пространство ядра небольшого сообщения), чтобы получить некоторую интуицию для латентности write
(признавая, что это может быть контекстно-зависимым). Я заметил существенную несогласованность между тестами, которые мне кажутся похожими, и мне очень любопытно выяснить, откуда эта разница. Я понимаю, что микробенчмарки могут быть проблематичными, но я все еще чувствую, что мне не хватает фундаментального понимания (так как разница в задержках составляет ~ 10x).
Настройка состоит в том, что у меня есть TCP-сервер C++, который принимает одно клиентское соединение (из другого процесса на одном CPU), а при подключении к клиенту делает 20 системных вызовов для write
в сокет, отправляя по одному байту за раз, Полный код сервера копируется в конце этого сообщения. Здесь вывод, который каждый раз write
с использованием boost/timer
(что добавляет шум ~ 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
в таймере, среднее значение составляет 2 микросекунды на write
, но первым вызовом всегда являются 15+ микрофоны. Почему это "разогревающее" явление?
Кроме того, я провел эксперимент, в котором между каждым вызовом write
я делаю некоторую блокировку работы ЦП (вычисление большого простого числа). Это приводит к замедлению всех вызовов 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
для копирования буферов из пользовательского пространства в пространство ядра. Если есть некоторый эффект коалесценции, который позволяет средней write
принимать только 2 микрофона, когда я делаю 10 000 последовательно, тогда было бы несправедливо оптимистично заключить, что латентность write
составляет 2 микрофона; кажется, что моя интуиция должна заключаться в том, что каждая write
занимает 20 микросекунд. Это кажется удивительно медленным для наименьшей задержки, которую вы можете получить (необработанный вызов write
на один байт) без байпаса ядра.
Последний фрагмент данных заключается в том, что когда я настраивал тест ping-pong между двумя процессами на моем компьютере (TCP-сервер и TCP-клиент), я в среднем 6 микрофонов за поездку в оба конца (включая read
, write
, а также как перемещение через локальную сеть). Это, похоже, противоречит задержкам в 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 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_NODELAY
, TCP_NOPUSH
, SO_SNDBUF
и SO_SNDLOWAT
с идеей, что это может помешать пакетной обработке (но я понимаю, что эта пакетная обработка происходит между буфером ядра и сетью, а не между пользовательским буфером и буфером ядра),
Вот код сервера для теста 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);
}
Вот код клиента для теста пинг-понга:
// #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 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);
}
}