在调查我们的 Python 代码库中的关键路径时,我们发现 ctypes 在延迟方面的行为非常不可预测。
再介绍一下我们的应用程序的背景。我们有许多进程,每个进程都通过共享内存进行通信。我们利用 Python 库multiprocessing.RawValue,并multiprocessing.RawArray在内部用于ctypes数据管理。在生产中运行它时,我们发现即使对这些共享数据类型进行简单的get()访问也需要大约 30-50 微秒,有时甚至需要 100 微秒,这非常慢。即使对于 Python 来说也是如此。
multiprocessing.RawValue
multiprocessing.RawArray
ctypes
get()
我创建了这个基本示例,它创建了一个ctype结构并公开了get()方法
ctype
import ctypes import sys import time import numpy as np import random from decimal import Decimal def get_time_ns(): return Decimal(str(time.time_ns())) class Point(ctypes.Structure): _fields_ = [("x", ctypes.c_int), ("y", ctypes.c_int)] def __init__(self, x, y): return super().__init__(x, y) def get(self): return self.x #return str(self.x) + "," + str(self.y) def benchmark(delay_mode): p = Point(10, 20) iters = 10 while iters: start_ts = get_time_ns() _ = p.get() end_ts = get_time_ns() print("Time: {} ns".format(end_ts - start_ts)) iters -= 1 if delay_mode == 1: time.sleep(random.uniform(0, 0.1)) benchmark(int(sys.argv[1]))
当我在非睡眠模式下运行此程序时,延迟数字如下
[root@centos-s-4vcpu-8gb-fra1-01 experiments]# python3.9 simple_ctype.py 0 Time: 9556 ns Time: 2246 ns Time: 1124 ns Time: 1174 ns Time: 1091 ns Time: 1126 ns Time: 1081 ns Time: 1066 ns Time: 1077 ns Time: 1138 ns
当我在睡眠模式下运行它时,延迟数字如下
[root@centos-s-4vcpu-8gb-fra1-01 experiments]# python3.9 simple_ctype.py 1 Time: 27233 ns Time: 27592 ns Time: 31687 ns Time: 32817 ns Time: 26234 ns Time: 32651 ns Time: 29468 ns Time: 36981 ns Time: 31313 ns Time: 34667 ns
使用的原因sleep是为了模拟我们的生产环境,其中应用程序所做的不仅仅是运行这个循环
sleep
有人能解释一下与上述热循环相比,中断时延迟增加 10-20 倍的原因吗?我最好的猜测是 CPU 缓存未命中,但这仍然不能解释这种延迟增加。我对 ctypes 实际上如何管理内存也感到很困惑。它只是简单的mallocormmap和 吗malloc?最后但并非最不重要的一点是,如果有人能帮助我们优化这一点,那就太好了。
malloc
mmap
系统信息:CentOS 7.9、4 核 CPU、16 GB RAM。taskset将特定 CPU 核心固定到脚本
taskset
仅供参考,我们已经知道 C++/Rust 在这种高精度性能方面比 Python 等高级语言更胜一筹,但考虑到时间敏感性和其他业务原因,我们希望在真正遇到语言障碍之前优化我们的 Python 代码以提高性能
在你的测试中,主要看到的延迟差异可能与以下几个方面相关:
在睡眠模式下,CPU 可能会进入省电模式(C-state),或者缓存中的数据可能被驱逐,需要从主内存中重新加载。
上下文切换和调度延迟:
在睡眠模式中调用 time.sleep(),即使只有 0.1 秒的随机延迟,也会导致操作系统将当前线程从 CPU 核心移除。线程恢复时,调度程序需要一些时间重新分配 CPU,这会增加延迟。
time.sleep()
ctypes 内存管理的开销:
ctypes.Structure
访问 ctypes 的字段可能涉及指针跳转和额外的解引用,增加开销。
高精度计时器开销:
time.time_ns()
如果你要继续使用 ctypes,可以尝试以下优化策略:
将循环中关键部分的 ctypes 调用移到 C 层次上处理,减少 Python 的运行时开销。例如,使用 ctypes.CDLL 调用一个简单的 C 函数来访问结构字段。
ctypes.CDLL
示例代码: ```c // simple.c #include
typedef struct { int32_t x; int32_t y; } Point;
int32_t get_x(Point* p) { return p->x; } ```
编译为共享库: bash gcc -shared -o simple.so -fPIC simple.c
bash gcc -shared -o simple.so -fPIC simple.c
然后在 Python 中使用: ```python import ctypes import time
# Load shared library lib = ctypes.CDLL(‘./simple.so’)
# Define Point structure class Point(ctypes.Structure): fields = [(“x”, ctypes.c_int32), (“y”, ctypes.c_int32)]
p = Point(10, 20) get_x = lib.get_x get_x.argtypes = [ctypes.POINTER(Point)] get_x.restype = ctypes.c_int32
# Benchmark start = time.time_ns() for _ in range(106): _ = get_x(ctypes.byref(p)) end = time.time_ns() print(f”Time per call: {(end - start) / 106} ns”) ```
如果共享内存是瓶颈,可以考虑替换 multiprocessing.RawValue 和 RawArray,例如: - 使用 numpy 的共享内存视图(np.frombuffer)。 - 使用更高效的共享内存库,如 shared_memory(Python 3.8+ 提供)。
RawArray
numpy
np.frombuffer
shared_memory
示例代码: ```python from multiprocessing.shared_memory import SharedMemory import numpy as np
shm = SharedMemory(create=True, size=8) buffer = np.ndarray((2,), dtype=np.int32, buffer=shm.buf) buffer[0], buffer[1] = 10, 20
# Access shared data def get_x(): return buffer[0] ```
cython
使用 cython 将关键路径的 Python 代码编译为 C,直接操作内存。
cython 示例: ```cython cdef struct Point: int x int y
cdef inline int get_x(Point* p): return p.x
def benchmark(): cdef Point p p.x = 10 p.y = 20 cdef int result for _ in range(1000000): result = get_x(&p) ```
编译后,这种实现可以显著减少访问延迟。
你的 time.time_ns() 和 Decimal 的结合可能本身就会引入一定的开销。考虑直接使用更轻量的计时工具:
Decimal
使用 time.perf_counter_ns(): python import time start = time.perf_counter_ns() # Your code here end = time.perf_counter_ns() print(f"Time: {end - start} ns")
time.perf_counter_ns()
python import time start = time.perf_counter_ns() # Your code here end = time.perf_counter_ns() print(f"Time: {end - start} ns")
使用专门的性能分析工具:
cProfile
line_profiler
perf
通过上述方法,你应该可以显著降低 ctypes 调用的延迟,并使代码更适合生产环境的高性能需求。