Perché il seguente codice è lento? E per lento intendo 100x-1000x lento. Esegue ripetutamente la lettura/scrittura direttamente su un socket TCP. La parte curiosa è che rimane lento solo se uso due chiamate di funzione sia per leggere che per scrivere come mostrato di seguito. Se cambio il server o il codice client per utilizzare una singola chiamata di funzione (come nei commenti), diventa super veloce.Perché il socket TCP rallenta se eseguito in più chiamate di sistema?
Codice frammento:
int main(...) {
int sock = ...; // open TCP socket
int i;
char buf[100000];
for(i=0;i<2000;++i)
{ if(amServer)
{ write(sock,buf,10);
// read(sock,buf,20);
read(sock,buf,10);
read(sock,buf,10);
}else
{ read(sock,buf,10);
// write(sock,buf,20);
write(sock,buf,10);
write(sock,buf,10);
}
}
close(sock);
}
Ci siamo imbattuti su questo in un programma più ampio, che è stato in realtà usando il buffering stdio. Diventò misteriosamente pigro nel momento in cui le dimensioni del carico utile superarono di poco le dimensioni del buffer. Poi ho fatto un po 'di ricerche con strace
e infine ho risolto il problema. Posso risolvere questo problema adottando strategie di buffering, ma mi piacerebbe davvero sapere cosa diavolo sta succedendo qui. Sulla mia macchina, passa da 0,030 s ad oltre un minuto sulla mia macchina (testato sia localmente che su macchine remote) quando cambio le due chiamate lette in una singola chiamata.
Questi test sono stati eseguiti su varie distribuzioni Linux e varie versioni del kernel. Stesso risultato
codice completamente eseguibile con boilerplate rete:
#include <netdb.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/ip.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
static int getsockaddr(const char* name,const char* port, struct sockaddr* res)
{
struct addrinfo* list;
if(getaddrinfo(name,port,NULL,&list) < 0) return -1;
for(;list!=NULL && list->ai_family!=AF_INET;list=list->ai_next);
if(!list) return -1;
memcpy(res,list->ai_addr,list->ai_addrlen);
freeaddrinfo(list);
return 0;
}
// used as sock=tcpConnect(...); ...; close(sock);
static int tcpConnect(struct sockaddr_in* sa)
{
int outsock;
if((outsock=socket(AF_INET,SOCK_STREAM,0))<0) return -1;
if(connect(outsock,(struct sockaddr*)sa,sizeof(*sa))<0) return -1;
return outsock;
}
int tcpConnectTo(const char* server, const char* port)
{
struct sockaddr_in sa;
if(getsockaddr(server,port,(struct sockaddr*)&sa)<0) return -1;
int sock=tcpConnect(&sa); if(sock<0) return -1;
return sock;
}
int tcpListenAny(const char* portn)
{
in_port_t port;
int outsock;
if(sscanf(portn,"%hu",&port)<1) return -1;
if((outsock=socket(AF_INET,SOCK_STREAM,0))<0) return -1;
int reuse = 1;
if(setsockopt(outsock,SOL_SOCKET,SO_REUSEADDR,
(const char*)&reuse,sizeof(reuse))<0) return fprintf(stderr,"setsockopt() failed\n"),-1;
struct sockaddr_in sa = { .sin_family=AF_INET, .sin_port=htons(port)
, .sin_addr={INADDR_ANY} };
if(bind(outsock,(struct sockaddr*)&sa,sizeof(sa))<0) return fprintf(stderr,"Bind failed\n"),-1;
if(listen(outsock,SOMAXCONN)<0) return fprintf(stderr,"Listen failed\n"),-1;
return outsock;
}
int tcpAccept(const char* port)
{
int listenSock, sock;
listenSock = tcpListenAny(port);
if((sock=accept(listenSock,0,0))<0) return fprintf(stderr,"Accept failed\n"),-1;
close(listenSock);
return sock;
}
void writeLoop(int fd,const char* buf,size_t n)
{
// Don't even bother incrementing buffer pointer
while(n) n-=write(fd,buf,n);
}
void readLoop(int fd,char* buf,size_t n)
{
while(n) n-=read(fd,buf,n);
}
int main(int argc,char* argv[])
{
if(argc<3)
{ fprintf(stderr,"Usage: round {server_addr|--} port\n");
return -1;
}
bool amServer = (strcmp("--",argv[1])==0);
int sock;
if(amServer) sock=tcpAccept(argv[2]);
else sock=tcpConnectTo(argv[1],argv[2]);
if(sock<0) { fprintf(stderr,"Connection failed\n"); return -1; }
int i;
char buf[100000] = { 0 };
for(i=0;i<4000;++i)
{
if(amServer)
{ writeLoop(sock,buf,10);
readLoop(sock,buf,20);
//readLoop(sock,buf,10);
//readLoop(sock,buf,10);
}else
{ readLoop(sock,buf,10);
writeLoop(sock,buf,20);
//writeLoop(sock,buf,10);
//writeLoop(sock,buf,10);
}
}
close(sock);
return 0;
}
EDIT: Questa versione è leggermente diversa dalle altre in quanto frammento di legge/scrive in un ciclo. Quindi, in questa versione, due scritture separate provocano automaticamente due chiamate read()
separate, anche se readLoop
viene chiamato una sola volta. Ma per il resto il problema rimane ancora.
Grazie, stavo cercando di controllare, ma non riuscivo a ricordare il nome dell'algoritmo di Nagle. Ma ho ancora una domanda ... le dimensioni particolari non fanno alcuna differenza qui. Puoi indicarmi un riepilogo delle euristiche utilizzate per attivare questo ordinariamente (cioè senza TCP_NODELAY)? – Samee
Per me, penso che sia più facile aggiungere TC_NODELAY piuttosto che fare confusione con la politica di buffering. – Samee
@Samee Sto ancora indagando su questo, ma hai ragione che non dipende dalla particolare dimensione. Aggiungendo 4 letture di 10 byte sul server e 2 scritture di 20 byte sul client, rallenta di nuovo (con 'TCP_NODELAY' disabilitato). Aggiornerò la mia risposta a breve con ulteriori dettagli. –