在使用 Cython(或 ctypes)调用的 Visual Studio 编译器上使用 OpenMP 的 C 扩展逐渐减慢直至停止

CSS*_*782 5 c openmp cython visual-studio python-3.x

我有一个 C 函数,它执行一些 I/O 操作和解码,我想从 python 脚本调用它。

C 函数在由 Visual Studio 命令行 C 编译器编译时工作正常,并且在禁用多线程的情况下通过 Cython 调用时也工作正常。但是,当使用 OpenMP 多线程调用时,在前几百万个循环中运行良好,但在接下来的几百万个循环中 CPU 使用率慢慢下降,直到最终停止并且不会失败,但也不会继续计算。

C文件如下:

//block_reader.c

#include "block_reader.h" //contains block_data_t, decode_block, get_block_data
#include <stdio.h>
#include <stdlib.h>

#define NTHREADS 8

int decode_blocks(block_data_t *block_data_array, int num_blocks, int *values){
  int block;
#pragma omp parallel for num_threads(NTHREADS)
  for(block=0; block<num_blocks; block++){
    decode_block(block_data_array[i], values);
  }

}

int main(int argc, char *argv[]) {
  int num_blocks = 250000, block_size = 4096;
  block_data_t *block_data_array = get_block_data();

  int *values = (long long *)malloc(num_blocks * block_size * sizeof(int));

  int i, block;
  for(i=0; i<1000; i++){
    printf("experiment #%d\n", i+1);

    decode_blocks(block_data_array, values)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

当在 Visual Studio x64 命令行上编译时cl /W3 -openmp block_reader.c block_helper.c zstd.lib,主函数循环一直进行实验 #1000,CPU 使用率始终为 90%(我的机器有 8 个逻辑线程,我不知道为什么它的上限为90% 在纯 C 中,当我从 openMP 编译指示中删除 num_threads(NTHREADS) 时,我遇到了同样的问题,但我并不真正担心它)。

然而,当我将它包装在 Cython 中并在 python 中循环时:

#block_reader_wrapper.pyx

from libc.stdlib cimport malloc
from libc.stdio cimport printf
cimport openmp
cimport block_reader_defns #contains block_data_t

import numpy as np
cimport numpy as np

cimport cython

@cython.boundscheck(False)  # Deactivate bounds checking.
@cython.wraparound(False)   # Deactivate negative indexing.
cpdef tuple read_blocks(block_data_array):

  cdef np.ndarray[np.int32_t, ndim=1] values = np.zeros(size, dtype=np.int32_t)
  cdef int[::1] values_view = values

  decode_blocks(block_data_array, len(block_data_array), num_blocks, &values_view[0])

  return values

cdef extern from "block_reader.h":
  int decode_blocks(char**, b_metadata*, unsigned int, unsigned long long*, long long*, int*)



#setup.block_reader_wrapper.py

from setuptools import setup, Extension
from Cython.Build import cythonize
import numpy

ext_modules = [
    Extension(
        "block_reader_wrapper",
        ["block_reader_wrapper.pyx", "block_reader.c", "block_helper.c"],
        libraries=["zstd"],
        library_dirs=["{dir}/vcpkg/installed/x64-windows/lib"],
        include_dirs=['{dir}/vcpkg/installed/x64-windows/include', numpy.get_include()],
        extra_compile_args=['/openmp', '-O2'], #Have tried -O2, -O3 and no optimization
        extra_link_args=['/openmp'], #always gets LINK : warning LNK4044: unrecognized option '/openmp'; ignored despite the docs asking for it https://cython.readthedocs.io/en/latest/src/userguide/parallelism.html
    )
]

setup(
    ext_modules = cythonize(ext_modules,
                            gdb_debug=True,
                            annotate=True,
                            )
)


#experiment.py

from block_reader_wrapper import read_blocks
from block_data_gen import get_block_data

for i in range(1000):
  print("experiment", i+1)
  read_blocks(get_block_data())
Run Code Online (Sandbox Code Playgroud)

我以 100% 的 CPU 使用率进行实验 #10(并且运行速度比 90% 上限的纯 C 快一点),但在实验 #11 - 实验 #16 之间,CPU 使用率以 1 个逻辑线程的增量缓慢下降资源,直到 CPU 使用率达到 1 个逻辑线程的底部,尽管我的任务管理器声称 python 使用了大约 20% 的 CPU 使用率,但该进程停止输出数据。内存使用率始终相当低 (~10%)。

我认为这一定与 Cython 的 OpenMP 链接有关,也许隐式限制了我可以传递给其工作线程的有效负载数量。

任何见解都将不胜感激,我需要它最终在 Windows和Ubuntu上工作,这就是我首先选择 openMP 的原因。

编辑1:根据 DavidW 的建议,我替换了:

cdef np.ndarray[np.int32_t, ndim=1] values = np.zeros(size, dtype=np.int32_t)
Run Code Online (Sandbox Code Playgroud)

和:

cdef array.array values, values_temp
values_temp = array.array('q', [])
values = array.clone(values_temp, size, zero=True)
Run Code Online (Sandbox Code Playgroud)

不幸的是这并没有解决问题。

编辑 2 和 3:在对“停止运行”的进程进行分析后,我发现很大一部分 CPU 时间都花在了等待上。free_base具体来说是malloc_base模块 ucrtbase.dll 中的函数

编辑 4:我用 ctypes 而不是 cython 重写了包装器,它利用了相同的 C -> Python API,所以存在同样的问题也许并不奇怪(尽管 ctypes 的停止速度大约是 Cython 的两倍) )。

VTune 总结:

Elapsed Time:   285419.416s
    CPU Time:   22708.709s
    Effective Time: 9230.924s
    Spin Time:  13477.785s
    Overhead Time:  0s
    Total Thread Count: 10
    Paused Time:    0s


Top Hotspots
    Function    Module  CPU Time
    free_base   ucrtbase.dll    9061.852s
    malloc_base ucrtbase.dll    8308.887s
    NtWaitForSingleObject   ntdll.dll   1283.721s
    func@0x180020020    USER32.dll  820.759s
    func@0x18001c630    tsc_block_reader.cp38-win_amd64.pyd 753.774s
    [Others]    N/A*    2479.716s

Effective CPU Utilization Histogram
    Simultaneously Utilized Logical CPUs    Elapsed Time    Utilization threshold

    0   279744.7901384001   Idle
    1   5446.2851121    Poor
    2   177.8078306 Poor
    3   40.3033061  Poor
    4   10.2292884  Poor
    5   0   Poor
    6   0   Poor
    7   0   Ok
    8   0   Ideal
Run Code Online (Sandbox Code Playgroud)

尽管它说大约 80% 的 CPU 时间处于空闲状态,但应该在 30 秒内完成的循环在 2 天后甚至没有完成,因此它远远超过 80% 的空闲时间。

看起来大部分空闲时间都花在ucrtbase.dll