我需要通过TCP连接与游戏进行交流(我不能更改其代码)。开发人员已经提供了可用于执行此操作的代码。但我得出的结论是,这段python代码太慢了,因为数据结构一次只能读取一个原语:
bool <- socket_stream.read(1)
int <- socket_stream.read(4)
double <- socket_stream.read(8)
我正在寻找一种漂亮的方式来做到这一点:
Structure <- socket_stream.read(N)
但是有一个问题:所需的缓冲区大小不是恒定的,例如对于游戏数据结构:
def read_from(stream: StreamWrapper) -> "Game":
"""Read Game from input stream
"""
my_id = stream.read_int()
players = []
for _ in range(stream.read_int()):
players_element = Player.read_from(stream)
players.append(players_element)
current_tick = stream.read_int()
units = []
for _ in range(stream.read_int()):
units_element = Unit.read_from(stream)
units.append(units_element)
loot = []
for _ in range(stream.read_int()):
loot_element = Loot.read_from(stream)
loot.append(loot_element)
projectiles = []
for _ in range(stream.read_int()):
projectiles_element = Projectile.read_from(stream)
projectiles.append(projectiles_element)
zone = Zone.read_from(stream)
sounds = []
for _ in range(stream.read_int()):
sounds_element = Sound.read_from(stream)
sounds.append(sounds_element)
它指的是其他结构,但它们更简单。这就是StreamWrapper的样子:
class StreamWrapper:
BOOL_FORMAT_STRUCT_PACK = struct.Struct("?").pack
INT_FORMAT_STRUCT_PACK = struct.Struct("<i").pack
LONG_FORMAT_STRUCT_PACK = struct.Struct("<q").pack
FLOAT_FORMAT_STRUCT_PACK = struct.Struct("<f").pack
DOUBLE_FORMAT_STRUCT_PACK = struct.Struct("<d").pack
BOOL_FORMAT_STRUCT_UNPACK = struct.Struct("?").unpack
INT_FORMAT_STRUCT_UNPACK = struct.Struct("<i").unpack
LONG_FORMAT_STRUCT_UNPACK = struct.Struct("<q").unpack
FLOAT_FORMAT_STRUCT_UNPACK = struct.Struct("<f").unpack
DOUBLE_FORMAT_STRUCT_UNPACK = struct.Struct("<d").unpack
# Reading primitives
def read_bool(self) -> bool:
return self.BOOL_FORMAT_STRUCT_UNPACK(self.stream.read(1))[0]
def read_int(self) -> int:
return self.INT_FORMAT_STRUCT_UNPACK(self.stream.read(4))[0]
def read_long(self) -> int:
return self.LONG_FORMAT_STRUCT_UNPACK(self.stream.read(8))[0]
def read_float(self) -> float:
return self.FLOAT_FORMAT_STRUCT_UNPACK(self.stream.read(4))[0]
def read_double(self) -> float:
return self.DOUBLE_FORMAT_STRUCT_UNPACK(self.stream.read(8))[0]
我想用np.ndarray(shape, dtype=some_structured_dtype, buffer=stream.read(length), offset=offset...)
或numpy.frombuffer(stream.read(length), dtype=some_structured_dtype, count=count, offset=offset)
但是我不知道如何把它做得漂亮,这样才能真正加快读取数据的速度。
运行函数:所有的.read_from(...)
,就像我上面展示的一样。.write_to()
也让我担心,但我会自己处理……
def run(self):
strategy = None
debug_interface = DebugInterface(self.reader, self.writer)
while True:
message = ServerMessage.read_from(self.reader)
if isinstance(message, ServerMessage.GetOrder):
order = strategy.get_order(message.player_view, debug_interface if message.debug_available else None)
ClientMessage.OrderMessage(order).write_to(self.writer)
self.writer.flush()
elif isinstance(message, ServerMessage.UpdateConstants):
strategy = TrackingStrategy(message.constants)
elif isinstance(message, ServerMessage.Finish):
strategy.finish()
break
elif isinstance(message, ServerMessage.DebugUpdate):
strategy.debug_update(message.displayed_tick, debug_interface)
ClientMessage.DebugUpdateDone().write_to(self.writer)
self.writer.flush()
else:
raise Exception("Unexpected server message")
每项读取数据(从1到8字节)在Python中效率不高。你应该用一种紧凑的方式来解码它们。这通常是可行的,但并不总是可行的。例如,如果你读取一个带有前缀大小的字符串,那么你必须首先读取该字符串的大小,如果字符串以零结束,那么在Python中开始有效地执行这一操作就会变得棘手(需要处理前瞻性缓冲区)。
Python (CPython)的默认实现是一个解释器,因此函数调用没有被优化(像本机代码那样),也没有属性获取。read_bool
函数非常昂贵,因为:
- 初始函数调用(
read_bool
) self.BOOL_FORMAT_STRUCT_UNPACK
的获取self.stream.read
(2个值)的获取- 2个函数调用(
self.stream.read
和self.BOOL_FORMAT_STRUCT_UNPACK
) 一个可能的系统调用到 - 输出对象(字节数组)的创建+引用计数
- 字节数组的索引和创建+整数的引用计数
read
(如果没有缓冲)事实上,在没有read
系统调用的情况下,在我的系统上调用read_bool
已经花费了大约160 ns…
struct.unpack('!BII', b'x14x47x94xABx27x74x29xC7x11')
。在我的机器上,这需要150 ns,但是要连续解码3个值。结构越大,加速越好。
使用Numpy数组逐个解码值不会更快。事实上,它肯定会更慢,因为Numpy没有为此进行优化,并且对Numpy的每个函数调用都有很大的开销(关于操作)。Numpy只有在处理相同类型的重复元素时才值得使用。更具体地说,函数view
和np.frombuffer
(只调用一次,因为它是昂贵的)可以帮助实现这一点。
如果由于协议的设计方式,您需要对每个缓冲区项进行解码,那么您需要使用本地代码。您可以使用Cython轻松地实现这一点。在这种情况下,您应该逐块读取TCP流这样可以避免系统调用(甚至调用libc)。当缓冲区足够大时,应该解码消息。