小能豆

在进程中断期间使用 python ctypes 导致高延迟的原因

py

在调查我们的 Python 代码库中的关键路径时,我们发现 ctypes 在延迟方面的行为非常不可预测。

再介绍一下我们的应用程序的背景。我们有许多进程,每个进程都通过共享内存进行通信。我们利用 Python 库multiprocessing.RawValue,并multiprocessing.RawArray在内部用于ctypes数据管理。在生产中运行它时,我们发现即使对这些共享数据类型进行简单的get()访问也需要大约 30-50 微秒,有时甚至需要 100 微秒,这非常慢。即使对于 Python 来说也是如此。

我创建了这个基本示例,它创建了一个ctype结构并公开了get()方法

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是为了模拟我们的生产环境,其中应用程序所做的不仅仅是运行这个循环

有人能解释一下与上述热循环相比,中断时延迟增加 10-20 倍的原因吗?我最好的猜测是 CPU 缓存未命中,但这仍然不能解释这种延迟增加。我对 ctypes 实际上如何管理内存也感到很困惑。它只是简单的mallocormmap和 吗malloc?最后但并非最不重要的一点是,如果有人能帮助我们优化这一点,那就太好了。

系统信息:CentOS 7.9、4 核 CPU、16 GB RAM。taskset将特定 CPU 核心固定到脚本

仅供参考,我们已经知道 C++/Rust 在这种高精度性能方面比 Python 等高级语言更胜一筹,但考虑到时间敏感性和其他业务原因,我们希望在真正遇到语言障碍之前优化我们的 Python 代码以提高性能


阅读 16

收藏
2024-11-17

共1个答案

小能豆

问题分析

在你的测试中,主要看到的延迟差异可能与以下几个方面相关:

  1. CPU 缓存未命中
  2. 在非睡眠模式下,代码执行在紧凑的热循环中,访问的内存很可能仍在 CPU 缓存中。
  3. 在睡眠模式下,CPU 可能会进入省电模式(C-state),或者缓存中的数据可能被驱逐,需要从主内存中重新加载。

  4. 上下文切换和调度延迟

  5. 在睡眠模式中调用 time.sleep(),即使只有 0.1 秒的随机延迟,也会导致操作系统将当前线程从 CPU 核心移除。线程恢复时,调度程序需要一些时间重新分配 CPU,这会增加延迟。

  6. ctypes 内存管理的开销

  7. ctypes 使用 Python 的 ctypes.Structure 数据结构进行管理,底层通过 malloc 分配内存,但其访问和调用涉及 Python 的 GIL(全局解释器锁)和其他运行时开销,尤其是类型检查和解引用。
  8. 访问 ctypes 的字段可能涉及指针跳转和额外的解引用,增加开销。

  9. 高精度计时器开销

  10. 使用 time.time_ns() 或类似高精度计时器可能本身具有开销,尤其在频繁调用时会被放大。

内存分配的优化

如果你要继续使用 ctypes,可以尝试以下优化策略:

1. 减少 Python 调用栈开销

将循环中关键部分的 ctypes 调用移到 C 层次上处理,减少 Python 的运行时开销。例如,使用 ctypes.CDLL 调用一个简单的 C 函数来访问结构字段。

示例代码
```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

然后在 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) / 10
6} ns”)
```

2. 使用更高效的共享数据结构

如果共享内存是瓶颈,可以考虑替换 multiprocessing.RawValueRawArray,例如:
- 使用 numpy 的共享内存视图(np.frombuffer)。
- 使用更高效的共享内存库,如 shared_memory(Python 3.8+ 提供)。

示例代码
```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]
```

3. 使用 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 的结合可能本身就会引入一定的开销。考虑直接使用更轻量的计时工具:

  1. 使用 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")

  2. 使用专门的性能分析工具

  3. cProfileline_profiler 来定位性能热点。
  4. perf(Linux 工具)分析进程的缓存行为和上下文切换。

总结与建议

  1. 你的主要延迟来源是 CPU 缓存未命中和上下文切换。
  2. 通过将性能关键路径下沉到 C/Cython,可以显著优化性能。
  3. 对于共享内存,考虑使用更轻量级的数据结构(如 numpyshared_memory)。
  4. 使用更高效的计时工具来避免测量本身引入的干扰。

通过上述方法,你应该可以显著降低 ctypes 调用的延迟,并使代码更适合生产环境的高性能需求。

2024-11-17