non-blocking socket.send slow (gevent)
Created originally on Bitbucket by jamadden (Jason Madden)
gevent implements a blocking socket.sendall
for non-blocking sockets with a simple loop over socket.send
, catching EWOULDBLOCK as needed. (This isn't necessarily specific to gevent, of course.) In benchmarks, this is substantially slower under PyPy than it is under CPython, around 5 to 6 times slower.
Here's a small example that reproduces the problem; start the script once with an argument to be the server and put it in the background, then again to be the client. (This is a simplified, non-gevent version of a benchmark Denis wrote; it's the only benchmark that PyPy is outperformed by CPython.)
#! /usr/bin/env python
from __future__ import print_function
import sys
import time
import socket
def serve():
server = socket.socket()
server.bind(("127.0.0.1", 9999))
server.listen(1)
while True:
client, _ = server.accept()
while client.recv(4096):
pass
def _sendall(conn, data):
data_memory = memoryview(data) # if memoryview is left out, CPython gets slow; makes no diff to PyPy
len_data_memory = len(data_memory)
data_sent = 0
while data_sent < len_data_memory:
try:
data_sent += conn._sock.send(data_memory[data_sent:])
except socket.error as ex:
if ex.args[0] == 35: # EWOULDBLOCK
continue
raise
def main():
length = 50 * 0x100000
data = b"x" * length
spent_total = 0
conn = socket.create_connection(("", 9999))
conn._sock.setblocking(0) # non-blocking is crucial
N = 20
for i in range(N):
start = time.time()
_sendall(conn, data)
spent = time.time() - start
print("%.2f MB/s" % (length / spent / 0x100000))
spent_total += spent
print("~ %.2f MB/s" % (length * N / spent_total / 0x100000))
if __name__ == "__main__":
if len(sys.argv) > 1:
serve()
else:
main()
On one machine, CPython sends at ~ 1160MB/s, while PyPy 2.6/2.7 sends at ~150MB/s.
The _sendall function is a simplified version of what gevent actually uses to implement socket.sendall
.
Interestingly, on CPython, if you take out the call to memoryview
and instead pass the raw string argument to socket.send
, it performs similarly to PyPy. This leads me to guess that it's something to do with pinning the buffer in memory repeatedly that's slowing PyPy down.
I've tried variations on how the data gets sliced to no avail. I have found that increasing the socket's SO_SNDBUF increases performance---using a very large buffer gets us about halfway to CPython performance.
Is there anything I can do as a maintainer of gevent to improve the performance of socket.sendall
? I'm not against using PyPy internal functions, I just couldn't find any to use :) Or should I recommend that users set large write buffers on their sockets? Or is this a "bug" in PyPy that can be improved?