调用socket.recv时,如果bufsize不是2的幂,会产生什么实际影响?

32
要在Python中从套接字读取数据,您需要调用socket.recv函数,该函数的签名如下:

socket.recv(bufsize[, flags])

python docs for socket.recv模糊地说明:

注意:为了与硬件和网络实际情况最佳匹配,bufsize的值应该是相对较小的2的幂,例如4096。

问题:“与硬件和网络实际情况最佳匹配”是什么意思?将bufsize设置为非2的幂的实际影响是什么?

我看到许多 其他 建议,使得这个读取长度成为2的幂次方。我也很清楚,在许多情况下,将数组长度设置为2的幂次方是有用的(在长度上进行位移/掩码操作,最优FFT数组大小等),但这些是应用程序相关的。我只是没有看到使用socket.recv时的一般原因。当然不足以成为Python文档中的具体建议。我也没有看到任何2的幂次方优化在底层Python代码中,使其成为Python特定的建议。

例如,如果您有一个协议,其中传入数据包的长度已经确定,那么最好只读取处理当前数据包所需的内容,否则可能会影响下一个数据包的处理,这将会很烦人。如果我正在处理的数据包只有42个字节未处理,我只会将缓冲区大小设置为42。

我错过了什么吗?当我不得不选择任意的缓冲区/数组大小时,通常(总是?)将长度设为2的幂次方,以防万一。这只是多年习惯而已。Python文档也只是受习惯支配吗?
这不仅适用于Python,但由于我特别提到了Python文档,因此我会标记它。

更新:我刚刚检查了内核级别上缓冲区的大小(或者至少我认为我检查了...我运行了cat /proc/sys/net/core/rmem_default),它是124928,不是2的幂。 rmem_max是131071,同样明显不是2的幂。

在更深入地研究后,我真的看不出2的幂推荐有什么好处。我准备把它称为虚假建议...

我还添加了tcpC标签,因为它们也相关。


我最好的猜测是,一些操作系统级别的网络堆栈可能会倾向于维护2的幂大小的缓冲区,出于内存管理的原因,但这似乎是一个毫无意义的历史遗物。也许笔记的主要重点是缓冲区大小应该在4KB左右,而不是4MB或4GB,而2的幂只是迷信。 - Russell Borogove
1
我的猜测是,非2的幂大小可能唯一可能产生的真正后果是,某些较低级别的代码可能会分配一个大小为下一个2的幂的缓冲区,而缓冲区的后面部分则不会被使用。这并不是一个问题,除非你的RAM非常短缺... - Jeremy Friesner
请查看此SO问题上的答案。它也涉及到recv(),这些答案可能有助于更全面地理解。它不会回答问题,但会提供一些更深入的见解。 - extraneon
最有可能是唯一重要的数字是:缓存行大小(通常为64字节,但始终是2的幂),以及页面大小(通常为4096字节,但始终是2的幂)。只要您是其中更相关的倍数,您就可能是理想的。 - o11c
请注意,由于缓存关联的奇怪性,通常最好不要使用恰好是2的幂。 - o11c
2个回答

9
我很确定“二次幂”建议是基于编辑错误而提出的,不应被视为要求。
该特定建议是作为回应Python issue #756104而添加到Python 2.5文档(并被移植到Python 2.4.3文档)的。报告人对socket.recv()使用了不合理大的缓冲区大小,这促使更新。
是Tim Peters引入了“二次幂”概念:
我觉得你可能是历史上唯一尝试将如此大的值传递给recv()函数的人 - 即使它可以工作,你几乎肯定会用尽内存来分配1.9GB的缓冲区空间。套接字是一种低级设施,并且通常会传递一个相对较小的2的幂次(以便与硬件和网络实际情况最佳匹配)。 (加粗为原文)。我曾经与Tim一起工作过,他在网络编程和硬件方面拥有丰富的经验,因此通常情况下,在做出这样的评论时,我会相信他的话。他特别“喜欢”Windows 95堆栈,他称其为煤矿中的金丝雀,因为它有能力在压力下失败。但请注意,他说的是“通常”,而不是必须使用2的幂次方。 正是这种措辞随后导致了文档更新:
这是一个文档错误,用户应该“警告”一下。 这曾经困扰过我,并且两个不同的人在 #python 中问到了这个问题,所以也许我们应该在 recv() 文档中加入类似以下的内容。
"""
为了与硬件和网络实际情况最佳匹配,"buffer" 的值应该是相对较小的2的幂次方,例如4096。
""" 如果您认为措辞正确,请将错误分配给我,我会处理它。 这里没有人质疑“2的幂次方”的说法,但编辑在几次回复中从“通常是”变成了“应该是”。 对我来说,那些提出文档更新的人更关心确保您使用一个“小缓冲区”,而不是它是否是2的幂次方。这并不是说这不是好的建议;任何与内核交互的低级缓冲区都受益于与内核数据结构的对齐。

但是,尽管可能存在一个神秘的堆栈,其中大小为2的幂的缓冲区更加重要,但我怀疑Tim Peters从来没有想过他的经验(即这是“常见做法”)会被铸造成如此坚定的术语。如果不同的缓冲区大小对您的特定用例更有意义,请忽略它。


非常好的回答,而且还是一个有趣的历史片段。谢谢! - Russ
5
是的,“2 的幂次方”在这里是一个误导性的信息。特定的套接字实现可能会偏向于某些缓冲区大小而不是其他大小,但那个“bug”报告的解决实际上是为了阻止发布者尝试使用多GB级别的缓冲区。顺便说一句,Win95的价值在于发现Python(和Zope)中的缺陷。它的线程和套接字很好,但与当时的类Unix系统相比具有极其不同的实用性(例如时间)。在Win95上运行压力测试揭示了大量合理的缺陷! - Tim Peters

3
关于:“如果您有一个协议,其中传入数据包的长度是已知的,那么仅读取处理当前数据包所需的数据显然是更好的选择,否则您可能会侵入下一个数据包,这将很烦人。”
这对应用程序开发者可能更可取,但对于底层网络堆栈来说可能效率低下。首先,它占用了可以用于其他网络I/O的套接字缓冲区空间。其次,每个recv()调用都意味着进入系统调用/内核空间,并且在转换时存在性能惩罚。尽可能多地从内核空间获取数据并使用尽可能少的系统调用将其移至用户空间,并在那里进行消息解析始终是更好的选择。这增加了应用程序代码和消息处理的复杂性,但可能是最有效的。
话虽如此,鉴于今天处理器的速度和可用内存数量,这对于大多数应用程序可能不是问题,但这是“旧日”网络应用程序的常见建议。
关于2的幂推荐对于用户空间应用程序我不确定。我曾看到由于对齐和页面大小等问题而要求驱动程序具备这些类型的要求,但除非它在某种程度上有助于将数据从内核缓冲区复制到用户缓冲区中,否则其对用户空间的影响不清楚。也许有更多操作系统开发知识的人可以发表评论。

好的回答!但是... Python 的 bufsize 参数与底层内核套接字缓冲区没有任何关系。bufsize 只是分配给内核 recv 调用以将读取的字节填充到其中的缓冲区(Python 字符串)的大小。也就是说,bufsize 不应该对底层网络堆栈产生“低效”。虽然这确实突出了如果 socket.recv 返回过早(慢/不连续到达传入数据??),用户空间缓冲区分配可能过多的问题。如果内核缓冲区有所有字节,则限制内核 recv 调用为一个,这很好。 - Russ
另外有趣的是,我刚刚检查了一下我的系统上内核接收缓冲区的大小(我想是这个),它是一个奇怪的值...124928。我将在问题中更新此信息,因为那里的非二次幂似乎也很相关。 - Russ
2
是的,recv()缓冲区大小与内核缓冲区大小无关。效率低下在于(a)让数据滞留在内核缓冲区意味着内核在任何时刻可用的网络缓冲区空间更少。理想情况下,您应该释放内核缓冲区并在用户缓冲区中执行消息解析,以及(b)不必要的系统调用是低效的。假设您收到两条42字节的消息并在等待中。最好执行一次recv(),获得84字节并在您的缓冲区中解析消息,而不是执行两次42字节的recv()。对于两条消息来说没有关系,但这变成了一个规模问题。 - ribram
我明白了......谢谢你的澄清。但是,我不太喜欢过度阅读,并在应用程序级别上有第二个缓冲层来解析它,这让我感到很不自在,但这主要是因为字符串的不可变性有时会导致额外的空间使用。也许我会尝试一下使用Python缓冲区(现在称为memoryview,就像新名称一样),以避免这种情况。 - Russ

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接