python3.6:socket.recv()与socket.recv_into()的性能



我一直在使用python3.6来捕获高速udp流,并对socket.recv()socket.recv_into()进行了实验。我期望CCD_ 4更快;预先分配的";CCD_ 5,而不是每次读取数据包并将其附加到列表时创建新的字符串。

我的测试场景是核心绑定的,我知道我正在丢弃一些数据包,并且通过SO_RCVBUF上的setsockopt有一个大的套接字接收缓冲区大小。我还关闭了垃圾收集器以避免随机中断。

下面的片段有类似的表现,这对我来说没有意义,我想知道是否有人能帮我指出我遗漏了什么。谢谢

pkts = []
while time.time() - t_start < 10.0:
pkt = s.recv(2048)
pkts.append(pkt)
num_recv_captured = len(pkts)

与。

buffer = bytearray(2048)
num_recv_into_captured = 0
while time.time() - t_start < 10.0:
s.recv_into(buffer, 2048)
num_recv_into_captured += 1

在这里,我看到num_recv_into_captured与核心绑定场景中的num_recv_captured相似,但预计num_recv_into_captured会大一点。

性能测量非常困难。你所看到的可能是由于你的测试方法有问题,也可能是结果太接近而不明显。

所以,首先看一下你试图比较的两种方法。您可能会认为,唯一的区别是第二个不需要分配新的缓冲区,这是一个真正的区别,也是关键的有意义的区别,但不是唯一的区别。如果这是唯一的区别,你会认为它会更快,但这并不是唯一的区别。第二种方法还采用了一个额外的动态鸭子类型参数,Python需要解析和处理该参数。这不应该花费那么多时间,但很难说它与分配2048字节所需的时间相比如何,这将取决于解释器使用的方法。Python使用全局内存池,在一个没有其他事情发生的紧密循环中,它很可能会在不调用任何操作系统函数的情况下一次又一次地快速释放和重新分配相同的内存。

这就引出了下一个问题,即虽然这两项操作的成本很难确定(也许其他人更清楚它们中的任何一项有多大意义),但它们与网络通信的规模并不完全相同。您看到的是纳米/微秒式的性能差异,因为它们与毫秒式的网络操作有关。你不仅要调用操作系统并等待IO,而且在接收数据的速度快于发送数据的速度的情况下,操作系统很可能会让你的进程进入睡眠状态,尤其是当你真的受到核心限制时。您还提到了数据包丢失,这不一定是确定性的。

如果你真的很关心这种规模的性能,你应该使用C/C++或Rust或其他允许低级别访问的语言,或者编写C/C++或Cython模块,并使用该模块与python直接使用C套接字库(如果你的目标平台是linux,你甚至可以使用recvmmsg来真正提高性能)。不过你可能不会。我并不反对为了实验而进行实验(事实上,当你问这样一个问题,而互联网上的人只是向你解释为什么不去麻烦,因为你不需要它或其他什么时,我觉得这很烦人,)所以如果是这样的话,你所学到的是,通常微观优化几乎没有什么区别。

如果你正试图决定在一个更大的项目中使用哪种方法;如果你有任何理由为了方便而喜欢一个而不是另一个,就用那个吧。如果你真的很关心表现,我会坚持recv_into。即使通话速度不比recv快。如果你有一个调用该方法的有意义的应用程序,它的内存特性将发挥作用,我希望在没有所有非常小的分配和取消分配的情况下,系统整体会更好地工作,这些分配和取消不太可能像在你的小基准测试循环中那样完美地排列。

编辑:在这种情况下,仅仅为了明确数据包丢失是不确定的,因为系统上正在进行的其他操作没有被准确地记录和复制。。。我想说,从理论意义上讲,它总是确定性的,但作为一个观察者,它实际上是不可知的。

编辑2:我突然想到你提到禁用垃圾收集。这只会禁用收集器,但基于引用计数的内存释放仍然应该发生,因此紧密的recv循环可能会一次又一次地释放和重新分配同一个内存块,因为它是由CPython而不是操作系统分配的,而且是一小部分内存,所以很可能会很快完成。

编辑3:太晚了。。。无论如何,我只是注意到,你正在将所有数据包添加到recv下的列表中,这样你就不会释放和重新分配内存——你只会让它们保持原样,并将内存地址存储在列表结构中,这应该是一个非常快的操作。不取消分配内存意味着你不会得到重复使用的相同地址,但这也意味着不需要取消分配,与去操作系统并返回填充缓冲区相比,分配额外的2048字节块仍然非常快。与任何操作系统建立的进程睡眠相比,这些操作也将相形见绌。

最新更新