NumPy-1-26-中文文档-二十二-

NumPy 1.26 中文文档(二十二)

原文:numpy.org/doc/

CPU/SIMD 优化

原文:numpy.org/doc/1.26/reference/simd/index.html

NumPy 具有灵活的工作机制,允许它利用 CPU 拥有的 SIMD 特性,在所有流行的平台上提供更快和更稳定的性能。目前,NumPy 支持 X86、IBM/Power、ARM7 和 ARM8 架构。

NumPy 中的优化过程是在三个层次上进行的:

  • 代码使用通用的内部函数来编写,这是一组类型、宏和函数,通过使用保护,将它们映射到每个支持的指令集上,只有编译器识别他们时才可以使用。这使我们能够为相同功能生成多个内核,其中每个生成的内核表示一个或多个特定 CPU 特性的指令集。第一个内核表示最小(基线)CPU 特性,而其他内核则表示附加的(分派的)CPU 特性。

  • 编译时,使用 CPU 构建选项来定义要支持的最低和附加特性,基于用户选择和编译器支持。适当的内部函数与平台/架构内部函数叠加,并编译多个内核。

  • 运行时导入时,对 CPU 进行探测以获得支持的 CPU 特性集。使用机制来获取指向最适合的内核的指针,并且这将是调用函数的内核。

注意

NumPy 社区在实施此项工作之前进行了深入讨论,请查看NEP-38以获得更多澄清。

  • CPU 构建选项

    • 描述

    • 快速入门

      • 我正在为本地使用构建 NumPy

      • 我不想支持旧的x86架构处理器

      • 我遇到了与上述情况相同的情况,但使用了ppc64架构

      • AVX512 特性有问题吗?

    • 支持的特性

      • 在 x86 上

      • 在 IBM/POWER 大端

      • 在 IBM/POWER 小端

      • 在 ARMv7/A32

      • 在 ARMv8/A64

      • 在 IBM/ZSYSTEM(S390X)

    • 特殊选项

    • 行为

    • 平台差异

      • 在 x86::Intel 编译器

      • 在 x86::Microsoft Visual C/C++

    • 构建报告

    • 运行时调度

  • CPU 调度器是如何工作的?

    • 1- Configuration

    • 2- 发现环境

    • 3- 验证请求的优化

    • 4- 生成主配置头文件

    • 5- Dispatch-able sources and configuration statements

CPU 构建选项

原文:numpy.org/doc/1.26/reference/simd/build-options.html

描述

以下选项主要用于更改针对特定 CPU 功能进行优化的默认行为:

  • --cpu-baseline:所需 CPU 功能的最小集合。

    默认值为 min,提供可以安全运行在处理器系列内广泛平台上的最小 CPU 功能。

    注意

    在运行时,如果目标 CPU 不支持指定的任何功能,则 NumPy 模块将无法加载(引发 Python 运行时错误)。

  • --cpu-dispatch:分派的一组额外 CPU 功能。

    默认值为 max -xop -fma4,启用所有 CPU 功能,除了 AMD 遗留功能(在 X86 的情况下)。

    注意

    在运行时,如果目标 CPU 不支持任何指定功能,则 NumPy 模块将跳过这些功能。

这些选项可以通过 distutils 命令 distutils.command.builddistutils.command.build_clibdistutils.command.build_ext 访问。它们接受一组 CPU 功能或收集几个功能的功能组,或者特殊选项执行一系列过程。

注意

如果用户未指定 build_clibbuild_ext,则将使用 build 的参数,其中也包含默认值。

自定义 build_extbuild_clib

cd /path/to/numpy
python setup.py build --cpu-baseline="avx2 fma3" install --user 

仅自定义 build_ext

cd /path/to/numpy
python setup.py build_ext --cpu-baseline="avx2 fma3" install --user 

仅自定义 build_clib

cd /path/to/numpy
python setup.py build_clib --cpu-baseline="avx2 fma3" install --user 

您还可以通过 PIP 命令自定义 CPU/构建选项:

pip install --no-use-pep517 --global-option=build \
--global-option="--cpu-baseline=avx2 fma3" \
--global-option="--cpu-dispatch=max" ./ 

快速开始

通常,默认设置不会强加可能在一些旧处理器上不可用的特定 CPU 功能。提高基线功能的上限通常会提高性能,也可能减小二进制文件大小。

下面是可能需要更改默认设置的最常见情况:

我正在为本地使用构建 NumPy

我不打算将构建结果导出给其他用户,也不打算针对与主机不同的 CPU 进行优化。

native 设置为基线,或者在您的平台不支持 native 选项的情况下手动指定 CPU 功能:

python setup.py build --cpu-baseline="native" bdist 

对于这种情况,使用额外的 CPU 功能构建 NumPy 并不是必要的,因为所有支持的功能已经在基线功能中定义:

python setup.py build --cpu-baseline=native --cpu-dispatch=none bdist 

注意

如果主机平台不支持 native,将引发致命错误。

我不想支持 x86 架构的旧处理器

由于大多数 CPU 现在至少支持 AVXF16C 功能,您可以使用:

python setup.py build --cpu-baseline="avx f16c" bdist 

注意

--cpu-baseline 强制组合所有暗示功能,因此无需添加 SSE 功能。

我遇到了与上述情况相同的问题,但是针对 ppc64 架构

那么将基线功能的上限提升到 Power8:

python setup.py build --cpu-baseline="vsx2" bdist 

遇到AVX512功能问题了吗?

你可能对包含AVX512或任何其他 CPU 功能有一些保留,想要排除已调度功能:

python setup.py build --cpu-dispatch="max -avx512f -avx512cd \
-avx512_knl -avx512_knm -avx512_skx -avx512_clx -avx512_cnl -avx512_icl" \
bdist 

支持的功能

功能的名称可以表示一个功能或一组功能,如下表所示,支持的功能取决于最低兴趣:

注意

以下功能可能不被所有编译器支持,而且一些编译器在涉及AVX512AVX2FMA3等功能时可能会产生不同的暗示功能集。查看平台差异获取更多详细信息。

在 x86 上

名称 暗示 收集
SSE SSE2
SSE2 SSE
SSE3 SSE SSE2
SSSE3 SSE SSE2 SSE3
SSE41 SSE SSE2 SSE3 SSSE3
POPCNT SSE SSE2 SSE3 SSSE3 SSE41
SSE42 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT
AVX SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42
XOP SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
FMA4 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
F16C SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
FMA3 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C
AVX2 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C
AVX512F SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2
AVX512CD SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F
AVX512_KNL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512ER AVX512PF
AVX512_KNM SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL AVX5124FMAPS AVX5124VNNIW AVX512VPOPCNTDQ
AVX512_SKX SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512VL AVX512BW AVX512DQ
AVX512_CLX SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512VNNI
AVX512_CNL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512IFMA AVX512VBMI
AVX512_ICL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512VBMI2 AVX512BITALG AVX512VPOPCNTDQ
AVX512_SPR SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL AVX512FP16

在 IBM/POWER 大端

名称 意味着
VSX
VSX2 VSX
VSX3 VSX VSX2
VSX4 VSX VSX2 VSX3

在 IBM/POWER 小端

名称 意味着
VSX VSX2
VSX2 VSX
VSX3 VSX VSX2
VSX4 VSX VSX2 VSX3

在 ARMv7/A32

名称 意味着
NEON
NEON_FP16 NEON
NEON_VFPV4 NEON NEON_FP16
ASIMD NEON NEON_FP16 NEON_VFPV4
ASIMDHP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDDP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDFHM NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP

在 ARMv8/A64

名称 意味着
NEON NEON_FP16 NEON_VFPV4 ASIMD
NEON_FP16 NEON NEON_VFPV4 ASIMD
NEON_VFPV4 NEON NEON_FP16 ASIMD
ASIMD NEON NEON_FP16 NEON_VFPV4
ASIMDHP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDDP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDFHM NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP

在 IBM/ZSYSTEM(S390X)

名称 意味着
VX
VXE VX

| VXE2 | VX VXE | ## 特殊选项

  • NONE: 不启用任何功能。

  • NATIVE: 启用主机 CPU 支持的所有 CPU 功能,此操作基于编译器标志(-march=native-xHost/QxHost

  • MIN: 启用可以安全运行在广泛平台上的最低 CPU 功能:

    对于 Arch 意味着
    x86(32 位模式) SSE SSE2
    x86_64 SSE SSE2 SSE3
    IBM/POWER(大端模式) NONE
    IBM/POWER(小端模式) VSX VSX2
    ARMHF NONE
    ARM64 A.K. AARCH64 NEON NEON_FP16 NEON_VFPV4 ASIMD
    IBM/ZSYSTEM(S390X) NONE
  • MAX: 启用编译器和平台支持的所有 CPU 功能。

  • Operators-/+: 移除或添加功能,与选项MAXMINNATIVE一起使用。

行为

  • CPU 功能和其他选项不区分大小写,例如:

    python setup.py build --cpu-dispatch="SSE41 avx2 FMA3" 
    
  • 请求的优化顺序不重要:

    python setup.py build --cpu-dispatch="SSE41 AVX2 FMA3"
    # equivalent to
    python setup.py build --cpu-dispatch="FMA3 AVX2 SSE41" 
    
  • 逗号、空格或‘+’都可以用作分隔符,例如:

    python setup.py build --cpu-dispatch="avx2 avx512f"
    # or
    python setup.py build --cpu-dispatch=avx2,avx512f
    # or
    python setup.py build --cpu-dispatch="avx2+avx512f" 
    

    所有工作,但如果使用空格,则参数应该用引号括起来或通过反斜杠转义。

  • --cpu-baseline结合了所有暗示的 CPU 功能,例如:

    python setup.py build --cpu-baseline=sse42
    # equivalent to
    python setup.py build --cpu-baseline="sse sse2 sse3 ssse3 sse41 popcnt sse42" 
    
  • 如果编译器本地标志-march=native-xHost/QxHost通过环境变量CFLAGS启用,则--cpu-baseline将被视为“本地”:

    export CFLAGS="-march=native"
    python setup.py install --user
    # is equivalent to
    python setup.py build --cpu-baseline=native install --user 
    
  • --cpu-baseline会将任何指定的不受目标平台或编译器支持的功能转义,而不是引发致命错误。

    注意

    由于--cpu-baseline结合了所有暗示的功能,将启用最大支持的暗示功能,而不是转义所有功能。例如:

    # Requesting `AVX2,FMA3` but the compiler only support **SSE** features
    python setup.py build --cpu-baseline="avx2 fma3"
    # is equivalent to
    python setup.py build --cpu-baseline="sse sse2 sse3 ssse3 sse41 popcnt sse42" 
    
  • --cpu-dispatch 不包含任何暗示的 CPU 特性,因此除非你想禁用其中一个或全部特性,否则必须添加它们:

    # Only dispatches AVX2 and FMA3
    python setup.py build --cpu-dispatch=avx2,fma3
    # Dispatches AVX and SSE features
    python setup.py build --cpu-baseline=ssse3,sse41,sse42,avx,avx2,fma3 
    
  • --cpu-dispatch 会跳过任何指定的基线特性,也会跳过目标平台或编译器不支持的特性,而不会引发致命错误。

最终,您应始终通过构建日志检查最终报告以验证启用的特性。有关更多详细信息,请参阅构建报告。

平台差异

一些特殊条件迫使我们在涉及某些编译器或架构时将某些特性链接在一起,导致无法单独构建它们。

这些条件可以分为两部分,如下所示:

架构兼容性

需要对一些已确保在同一架构的后续世代中支持的 CPU 特性进行对齐的情况,有些情况如下:

  • 在 ppc64le 上,VSX(ISA 2.06)VSX2(ISA 2.07) 相���暗示,因为支持小端模式的第一代是 Power-8(ISA 2.07)

  • 在 AArch64 上,NEON NEON_FP16 NEON_VFPV4 ASIMD 相互暗示,因为它们是硬件基线的一部分。

例如:

# On ARMv8/A64, specify NEON is going to enable Advanced SIMD
# and all predecessor extensions
python setup.py build --cpu-baseline=neon
# which equivalent to
python setup.py build --cpu-baseline="neon neon_fp16 neon_vfpv4 asimd" 

注意

请仔细查看支持的特性,以确定彼此暗示的特性。

编译兼容性

一些编译器不提供对所有 CPU 特性的独立支持。例如Intel的编译器不为AVX2FMA3提供单独的标志,这是有道理的,因为所有支持AVX2的 Intel CPU 也支持FMA3,但这种方法与其他x86 CPU(如AMDVIA)不兼容。

例如:

# Specify AVX2 will force enables FMA3 on Intel compilers
python setup.py build --cpu-baseline=avx2
# which equivalent to
python setup.py build --cpu-baseline="avx2 fma3" 

以下表格仅显示一些编译器对通用上下文施加的差异,这些差异在支持的特性表格中已经显示:

注意

有删除线的特性名称表示不支持的 CPU 特性。

在 x86::Intel 编译器

名称 暗示 收集
FMA3 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2
AVX2 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3
AVX512F SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD
XOP SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
FMA4 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
AVX512_SPR SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL AVX512FP16

在 x86::Microsoft Visual C/C++

名称 暗示 收集
FMA3 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2
AVX2 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3
AVX512F SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD AVX512_SKX
AVX512CD SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512_SKX
AVX512_KNL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512ER AVX512PF
AVX512_KNM SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL AVX5124FMAPS AVX5124VNNIW AVX512VPOPCNTDQ

| AVX512_SPR | SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL | AVX512FP16 | ## 构建报告

在大多数情况下,CPU 构建选项不会产生导致构建挂起的致命错误。在构建日志中可能出现的大多数错误都是由于编译器缺少某些预期的 CPU 功能而产生的严重警告。

因此,我们强烈建议检查最终的报告日志,了解启用了哪些 CPU 功能以及哪些没有启用。

您可以在构建日志的末尾找到 CPU 优化的最终报告,以下是在 x86_64/gcc 上的展示方式:

########### EXT COMPILER OPTIMIZATION ###########
Platform  :
  Architecture:  x64
  Compiler  :  gcc

CPU  baseline  :
  Requested  :  'min'
  Enabled  :  SSE  SSE2  SSE3
  Flags  :  -msse  -msse2  -msse3
  Extra  checks:  none

CPU  dispatch  :
  Requested  :  'max -xop -fma4'
  Enabled  :  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C  FMA3  AVX2  AVX512F  AVX512CD  AVX512_KNL  AVX512_KNM  AVX512_SKX  AVX512_CLX  AVX512_CNL  AVX512_ICL
  Generated  :
  :
  SSE41  :  SSE  SSE2  SSE3  SSSE3
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1
  Extra  checks:  none
  Detect  :  SSE  SSE2  SSE3  SSSE3  SSE41
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithmetic.dispatch.c
  :  numpy/core/src/umath/_umath_tests.dispatch.c
  :
  SSE42  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2
  Extra  checks:  none
  Detect  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42
  :  build/src.linux-x86_64-3.9/numpy/core/src/_simd/_simd.dispatch.c
  :
  AVX2  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2  -mavx  -mf16c  -mavx2
  Extra  checks:  none
  Detect  :  AVX  F16C  AVX2
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithm_fp.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithmetic.dispatch.c
  :  numpy/core/src/umath/_umath_tests.dispatch.c
  :
  (FMA3  AVX2)  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2  -mavx  -mf16c  -mfma  -mavx2
  Extra  checks:  none
  Detect  :  AVX  F16C  FMA3  AVX2
  :  build/src.linux-x86_64-3.9/numpy/core/src/_simd/_simd.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_exponent_log.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_trigonometric.dispatch.c
  :
  AVX512F  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C  FMA3  AVX2
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2  -mavx  -mf16c  -mfma  -mavx2  -mavx512f
  Extra  checks:  AVX512F_REDUCE
  Detect  :  AVX512F
  :  build/src.linux-x86_64-3.9/numpy/core/src/_simd/_simd.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithm_fp.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithmetic.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_exponent_log.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_trigonometric.dispatch.c
  :
  AVX512_SKX  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C  FMA3  AVX2  AVX512F  AVX512CD
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2  -mavx  -mf16c  -mfma  -mavx2  -mavx512f  -mavx512cd  -mavx512vl  -mavx512bw  -mavx512dq
  Extra  checks:  AVX512BW_MASK  AVX512DQ_MASK
  Detect  :  AVX512_SKX
  :  build/src.linux-x86_64-3.9/numpy/core/src/_simd/_simd.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithmetic.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_exponent_log.dispatch.c
CCompilerOpt.cache_flush[804]  :  write  cache  to  path  ->  /home/seiko/work/repos/numpy/build/temp.linux-x86_64-3.9/ccompiler_opt_cache_ext.py

########### CLIB COMPILER OPTIMIZATION ###########
Platform  :
  Architecture:  x64
  Compiler  :  gcc

CPU  baseline  :
  Requested  :  'min'
  Enabled  :  SSE  SSE2  SSE3
  Flags  :  -msse  -msse2  -msse3
  Extra  checks:  none

CPU  dispatch  :
  Requested  :  'max -xop -fma4'
  Enabled  :  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C  FMA3  AVX2  AVX512F  AVX512CD  AVX512_KNL  AVX512_KNM  AVX512_SKX  AVX512_CLX  AVX512_CNL  AVX512_ICL
  Generated  :  none 

对于build_extbuild_clib的每个单独报告都包含几个部分,每个部分都有几个值,表示以下内容:

平台

  • 架构:目标 CPU 的架构名称。它应该是x86x64ppc64ppc64learmhfaarch64s390xunknown中的一个。

  • 编译器:编译器名称。它应该是 gcc、clang、msvc、icc、iccw 或类 Unix 的其中一个。

CPU 基线

  • 请求的:作为--cpu-baseline的特定功能和选项。

  • 已启用:最终启用的 CPU 功能集。

  • 标志:用于编译所有 NumPy C/C++ 源文件的编译器标志,除了用于生成分派功能的二进制对象的临时源文件。

  • 额外检查:激活与已启用功能相关的某些功能或内部函数的列表,对于开发 SIMD 内核时进行调试非常有用。

CPU 分派

  • 请求的:作为--cpu-dispatch的特定功能和选项。

  • 已启用:最终启用的 CPU 功能集。

  • 生成的:在此属性的下一行的开头,显示已生成优化的功能,以几个部分的形式显示,具有类似属性的解释如下:

    • 一个或多个分派功能:隐含的 CPU 功能。

    • 标志:用于这些功能的编译器标志。

    • 额外检查:类似于基线,但适用于这些分派功能。

    • 检测:需要在运行时检测以执行生成的优化的一组 CPU 功能。

    • 在上述属性之后并以单独一行的‘:’结尾的行,代表定义生成的优化的 c/c++ 源文件的路径。 ## 运行时分派

导入 NumPy 会触发对可分派功能集中的可用 CPU 功能进行扫描。这可以通过将环境变量NPY_DISABLE_CPU_FEATURES设置为逗号、制表符或空格分隔的功能列表来进一步限制。如果解析失败或未启用该功能,将引发错误。例如,在x86_64上,这将禁用AVX2FMA3

NPY_DISABLE_CPU_FEATURES="AVX2,FMA3" 

如果特性不可用,将发出警告。

描述

以下选项主要用于更改针对特定 CPU 特性的优化的默认行为:

  • --cpu-baseline:所需 CPU 特性的最小集。

    默认值为min,提供可以安全运行在处理器系列内广泛范围平台上的最小 CPU 特性。

    注意

    在运行时,如果目标 CPU 不支持任何指定特性,则 NumPy 模块将无法加载(引发 Python 运行时错误)。

  • --cpu-dispatch:分派的一组额外的 CPU 特性。

    默认值为max -xop -fma4,启用所有 CPU 特性,除了 AMD 遗留特性(在 X86 的情况下)。

    注意

    在运行时,NumPy 模块将跳过目标 CPU 中不可用的任何指定特性。

这些选项可通过distutils命令distutils.command.builddistutils.command.build_clibdistutils.command.build_ext访问,它们接受一组 CPU 特性或收集几个特性的特性组或特殊选项执行一系列过程。

注意

如果用户未指定build_clibbuild_ext,则将使用build的参数,这也包含默认值。

同时自定义build_extbuild_clib

cd /path/to/numpy
python setup.py build --cpu-baseline="avx2 fma3" install --user 

仅自定义build_ext

cd /path/to/numpy
python setup.py build_ext --cpu-baseline="avx2 fma3" install --user 

仅自定义build_clib

cd /path/to/numpy
python setup.py build_clib --cpu-baseline="avx2 fma3" install --user 

您还可以通过 PIP 命令自定义 CPU/构建选项:

pip install --no-use-pep517 --global-option=build \
--global-option="--cpu-baseline=avx2 fma3" \
--global-option="--cpu-dispatch=max" ./ 

快速开始

通常,默认设置往往不会强加一些可能在一些旧处理器上不可用的 CPU 特性。提高基线特性的上限通常会提高性能,也可能减小二进制文件大小。

以下是可能需要更改默认设置的最常见情况:

我正在为本地使用构建 NumPy

我也不打算将构建导出给其他用户或针对与主机不同的 CPU。

本机设置为基线,或者在您的平台不支持本机选项的情况下手动指定 CPU 特性:

python setup.py build --cpu-baseline="native" bdist 

对于这种情况,使用额外的 CPU 特性构建 NumPy 并不是必要的,因为所有支持的特性已经在基线特性中定义:

python setup.py build --cpu-baseline=native --cpu-dispatch=none bdist 

注意

如果主机平台不支持本机,将引发致命错误。

我不想支持x86架构的旧处理器

由于如今大多数 CPU 至少支持AVXF16C特性,您可以使用:

python setup.py build --cpu-baseline="avx f16c" bdist 

注意

--cpu-baseline强制组合所有隐含的特性,因此不需要添加 SSE 特性。

我遇到了与上述相同的情况,但是使用ppc64架构

然后将基线特性的上限提高到 Power8:

python setup.py build --cpu-baseline="vsx2" bdist 

遇到AVX512特性的问题?

您可能对包含AVX512或任何其他 CPU 特性有所保留,并希望从分派的特性中排除:

python setup.py build --cpu-dispatch="max -avx512f -avx512cd \
-avx512_knl -avx512_knm -avx512_skx -avx512_clx -avx512_cnl -avx512_icl" \
bdist 

我正在为本地使用构建 NumPy

我不打算将构建导出给其他用户或针对与主机不同的 CPU 进行目标定位。

设置native为基线,或者在您的平台不支持选项native的情况下手动指定 CPU 特性:

python setup.py build --cpu-baseline="native" bdist 

对于这种情况,构建 NumPy 时不需要额外的 CPU 特性,因为所有支持的特性已经在基线特性中定义:

python setup.py build --cpu-baseline=native --cpu-dispatch=none bdist 

注意

如果主机平台不支持native,将会引发致命错误。

我不想支持x86架构的旧处理器

由于大多数 CPU 现在至少支持AVXF16C特性,您可以使用:

python setup.py build --cpu-baseline="avx f16c" bdist 

注意

--cpu-baseline强制组合所有暗示的特性,因此不需要添加 SSE 特性。

我遇到了与上述相同的情况,但是使用ppc64架构

然后将基线特性的上限提高到 Power8:

python setup.py build --cpu-baseline="vsx2" bdist 

遇到AVX512特性的问题?

您可能对包含AVX512或任何其他 CPU 特性有所保留,并希望从分派的特性中排除:

python setup.py build --cpu-dispatch="max -avx512f -avx512cd \
-avx512_knl -avx512_knm -avx512_skx -avx512_clx -avx512_cnl -avx512_icl" \
bdist 

支持的特性

特性的名称可以表示一个特性或一组特性,如下表所示,支持的特性取决于最低的兴趣:

注意

以下特性可能不被所有编译器支持,而且一些编译器在涉及AVX512AVX2FMA3等特性时可能会产生不同的暗示特性集。有关更多详细信息,请参阅平台差异。

在 x86 上

名称 意味着 收集
SSE SSE2
SSE2 SSE
SSE3 SSE SSE2
SSSE3 SSE SSE2 SSE3
SSE41 SSE SSE2 SSE3 SSSE3
POPCNT SSE SSE2 SSE3 SSSE3 SSE41
SSE42 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT
AVX SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42
XOP SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
FMA4 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
F16C SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
FMA3 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C
AVX2 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C
AVX512F SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2
AVX512CD SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F
AVX512_KNL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512ER AVX512PF
AVX512_KNM SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL AVX5124FMAPS AVX5124VNNIW AVX512VPOPCNTDQ
AVX512_SKX SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512VL AVX512BW AVX512DQ
AVX512_CLX SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512VNNI
AVX512_CNL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512IFMA AVX512VBMI
AVX512_ICL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512VBMI2 AVX512BITALG AVX512VPOPCNTDQ
AVX512_SPR SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL AVX512FP16

在 IBM/POWER 大端

名称 意味着
VSX
VSX2 VSX
VSX3 VSX VSX2
VSX4 VSX VSX2 VSX3

在 IBM/POWER 小端

名称 意味着
VSX VSX2
VSX2 VSX
VSX3 VSX VSX2
VSX4 VSX VSX2 VSX3

在 ARMv7/A32

名称 意味着
NEON
NEON_FP16 NEON
NEON_VFPV4 NEON NEON_FP16
ASIMD NEON NEON_FP16 NEON_VFPV4
ASIMDHP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDDP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDFHM NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP

在 ARMv8/A64

名称 意味着
NEON NEON_FP16 NEON_VFPV4 ASIMD
NEON_FP16 NEON NEON_VFPV4 ASIMD
NEON_VFPV4 NEON NEON_FP16 ASIMD
ASIMD NEON NEON_FP16 NEON_VFPV4
ASIMDHP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDDP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDFHM NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP

在 IBM/ZSYSTEM(S390X)

名称 意味着
VX
VXE VX
VXE2 VX VXE

在 x86

名称 意味着 收集
SSE SSE2
SSE2 SSE
SSE3 SSE SSE2
SSSE3 SSE SSE2 SSE3
SSE41 SSE SSE2 SSE3 SSSE3
POPCNT SSE SSE2 SSE3 SSSE3 SSE41
SSE42 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT
AVX SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42
XOP SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
FMA4 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
F16C SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
FMA3 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C
AVX2 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C
AVX512F SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2
AVX512CD SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F
AVX512_KNL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512ER AVX512PF
AVX512_KNM SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL AVX5124FMAPS AVX5124VNNIW AVX512VPOPCNTDQ
AVX512_SKX SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512VL AVX512BW AVX512DQ
AVX512_CLX SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512VNNI
AVX512_CNL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512IFMA AVX512VBMI
AVX512_ICL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512VBMI2 AVX512BITALG AVX512VPOPCNTDQ
AVX512_SPR SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL AVX512FP16

在 IBM/POWER 大端

名称 含义
VSX
VSX2 VSX
VSX3 VSX VSX2
VSX4 VSX VSX2 VSX3

在 IBM/POWER 小端

名称 含义
VSX VSX2
VSX2 VSX
VSX3 VSX VSX2
VSX4 VSX VSX2 VSX3

在 ARMv7/A32

名称 含义
NEON
NEON_FP16 NEON
NEON_VFPV4 NEON NEON_FP16
ASIMD NEON NEON_FP16 NEON_VFPV4
ASIMDHP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDDP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDFHM NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP

在 ARMv8/A64

名称 含义
NEON NEON_FP16 NEON_VFPV4 ASIMD
NEON_FP16 NEON NEON_VFPV4 ASIMD
NEON_VFPV4 NEON NEON_FP16 ASIMD
ASIMD NEON NEON_FP16 NEON_VFPV4
ASIMDHP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDDP NEON NEON_FP16 NEON_VFPV4 ASIMD
ASIMDFHM NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP

在 IBM/ZSYSTEM(S390X)

名称 含义
VX
VXE VX
VXE2 VX VXE

特殊选项

  • NONE: 不启用任何功能。

  • NATIVE: 启用主机 CPU 支持的所有 CPU 功能,此操作基于编译器标志(-march=native-xHost/QxHost

  • MIN: 启用最小的 CPU 功能,可以安全地在各种平台上运行:

    对架构 含义
    x86(32 位模式) SSE SSE2
    x86_64 SSE SSE2 SSE3
    IBM/POWER(大端模式) NONE
    IBM/POWER(小端模式) VSX VSX2
    ARMHF NONE
    ARM64 A.K. AARCH64 NEON NEON_FP16 NEON_VFPV4 ASIMD
    IBM/ZSYSTEM(S390X) NONE
  • MAX: 通过编译器和平台启用所有支持的 CPU 特性。

  • Operators-/+:删除或添加特性,与选项MAXMINNATIVE一起使用。

行为

  • CPU 特性和其他选项不区分大小写,例如:

    python setup.py build --cpu-dispatch="SSE41 avx2 FMA3" 
    
  • 请求的优化顺序无关紧要:

    python setup.py build --cpu-dispatch="SSE41 AVX2 FMA3"
    # equivalent to
    python setup.py build --cpu-dispatch="FMA3 AVX2 SSE41" 
    
  • 分隔符可以使用逗号、空格或‘+’,例如:

    python setup.py build --cpu-dispatch="avx2 avx512f"
    # or
    python setup.py build --cpu-dispatch=avx2,avx512f
    # or
    python setup.py build --cpu-dispatch="avx2+avx512f" 
    

    所有都有效,但如果使用了空格,则参数应该用引号括起来或通过反斜杠进行转义。

  • --cpu-baseline结合了所有隐含的 CPU 特性,例如:

    python setup.py build --cpu-baseline=sse42
    # equivalent to
    python setup.py build --cpu-baseline="sse sse2 sse3 ssse3 sse41 popcnt sse42" 
    
  • 如果通过环境变量CFLAGS启用编译器本机标志-march=native-xHost/QxHost,则--cpu-baseline将被视为“本机”:

    export CFLAGS="-march=native"
    python setup.py install --user
    # is equivalent to
    python setup.py build --cpu-baseline=native install --user 
    
  • --cpu-baseline逃避任何指定的特性,如果目标平台或编译器不支持,则不会引发致命错误。

    注意

    由于--cpu-baseline结合了所有隐含的特性,所以将启用隐含特性中支持的最大特性,而不是逃避所有特性。例如:

    # Requesting `AVX2,FMA3` but the compiler only support **SSE** features
    python setup.py build --cpu-baseline="avx2 fma3"
    # is equivalent to
    python setup.py build --cpu-baseline="sse sse2 sse3 ssse3 sse41 popcnt sse42" 
    
  • --cpu-dispatch不结合任何隐含的 CPU 特性,因此除非您想要禁用其中一个或全部特性,否则必须添加它们:

    # Only dispatches AVX2 and FMA3
    python setup.py build --cpu-dispatch=avx2,fma3
    # Dispatches AVX and SSE features
    python setup.py build --cpu-baseline=ssse3,sse41,sse42,avx,avx2,fma3 
    
  • --cpu-dispatch逃避任何指定的基线特性,同时也逃避目标平台或编译器不支持的任何特性,而不会引发致命错误。

最终,您应该始终通过构建日志检查最终报告以验证启用的特性。有关更多详细信息,请参阅构建报告。

平台差异

在某些特殊情况下,当涉及到某些编译器或架构时,我们被迫将某些特性链接在一起,导致无法单独构建它们。

这些条件可以分为两部分,如下所示:

架构兼容性

需要对某些 CPU 特性进行对齐,这些特性被保证在同一架构的连续几代中都会支持,一些情况如下:

  • 在 ppc64le 上,VSX(ISA 2.06)VSX2(ISA 2.07)互相隐含,因为第一代支持小端模式的是 Power-8(ISA 2.07)

  • 在 AArch64 上,NEON NEON_FP16 NEON_VFPV4 ASIMD互相隐含,因为它们是硬件基线的一部分。

例如:

# On ARMv8/A64, specify NEON is going to enable Advanced SIMD
# and all predecessor extensions
python setup.py build --cpu-baseline=neon
# which equivalent to
python setup.py build --cpu-baseline="neon neon_fp16 neon_vfpv4 asimd" 

注意

请仔细查看支持的特性,以确定互相隐含的特性。

编译兼容性

一些编译器不提供对所有 CPU 特性的独立支持。例如,英特尔的编译器不为AVX2FMA3提供单独的标志,这是有道理的,因为所有带有AVX2的英特尔 CPU 也支持FMA3,但这种方法与其他x86 CPU(如AMDVIA)不兼容。

例如:

# Specify AVX2 will force enables FMA3 on Intel compilers
python setup.py build --cpu-baseline=avx2
# which equivalent to
python setup.py build --cpu-baseline="avx2 fma3" 

以下表格仅显示一些编译器对支持特性表中显示的一般上下文施加的差异:

带有删除线的特性名称代表不支持的 CPU 特性。

在 x86::Intel 编译器上

名称 暗示 收集
FMA3 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2
AVX2 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3
AVX512F SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD
XOP SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
FMA4 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
AVX512_SPR SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL AVX512FP16

在 x86::Microsoft Visual C/C++ 上

名称 暗示 收集
FMA3 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2
AVX2 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3
AVX512F SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD AVX512_SKX
AVX512CD SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512_SKX
AVX512_KNL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512ER AVX512PF
AVX512_KNM SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL AVX5124FMAPS AVX5124VNNIW AVX512VPOPCNTDQ
AVX512_SPR SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL AVX512FP16

在 x86::Intel 编译器上

名称 暗示 收集
FMA3 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2
AVX2 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3
AVX512F SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD
XOP SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
FMA4 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX
AVX512_SPR SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL AVX512FP16

在 x86::Microsoft Visual C/C++ 上

名称 暗示 收集
FMA3 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2
AVX2 SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3
AVX512F SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD AVX512_SKX
AVX512CD SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512_SKX
AVX512_KNL SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512ER AVX512PF
AVX512_KNM SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX5124FMAPS AVX5124VNNIW AVX512VPOPCNTDQ
AVX512_SPR SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL AVX512FP16

构建报告

在大多数情况下,CPU 构建选项不会产生导致构建挂起的致命错误。在构建日志中可能出现的大多数错误都是由于编译器缺少某些预期的 CPU 功能而产生的严重警告。

因此,我们强烈建议检查最终报告日志,了解启用了哪些 CPU 功能以及哪些没有。

您可以在构建日志的末尾找到 CPU 优化的最终报告,以下是在 x86_64/gcc 上的展示方式:

########### EXT COMPILER OPTIMIZATION ###########
Platform  :
  Architecture:  x64
  Compiler  :  gcc

CPU  baseline  :
  Requested  :  'min'
  Enabled  :  SSE  SSE2  SSE3
  Flags  :  -msse  -msse2  -msse3
  Extra  checks:  none

CPU  dispatch  :
  Requested  :  'max -xop -fma4'
  Enabled  :  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C  FMA3  AVX2  AVX512F  AVX512CD  AVX512_KNL  AVX512_KNM  AVX512_SKX  AVX512_CLX  AVX512_CNL  AVX512_ICL
  Generated  :
  :
  SSE41  :  SSE  SSE2  SSE3  SSSE3
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1
  Extra  checks:  none
  Detect  :  SSE  SSE2  SSE3  SSSE3  SSE41
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithmetic.dispatch.c
  :  numpy/core/src/umath/_umath_tests.dispatch.c
  :
  SSE42  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2
  Extra  checks:  none
  Detect  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42
  :  build/src.linux-x86_64-3.9/numpy/core/src/_simd/_simd.dispatch.c
  :
  AVX2  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2  -mavx  -mf16c  -mavx2
  Extra  checks:  none
  Detect  :  AVX  F16C  AVX2
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithm_fp.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithmetic.dispatch.c
  :  numpy/core/src/umath/_umath_tests.dispatch.c
  :
  (FMA3  AVX2)  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2  -mavx  -mf16c  -mfma  -mavx2
  Extra  checks:  none
  Detect  :  AVX  F16C  FMA3  AVX2
  :  build/src.linux-x86_64-3.9/numpy/core/src/_simd/_simd.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_exponent_log.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_trigonometric.dispatch.c
  :
  AVX512F  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C  FMA3  AVX2
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2  -mavx  -mf16c  -mfma  -mavx2  -mavx512f
  Extra  checks:  AVX512F_REDUCE
  Detect  :  AVX512F
  :  build/src.linux-x86_64-3.9/numpy/core/src/_simd/_simd.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithm_fp.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithmetic.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_exponent_log.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_trigonometric.dispatch.c
  :
  AVX512_SKX  :  SSE  SSE2  SSE3  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C  FMA3  AVX2  AVX512F  AVX512CD
  Flags  :  -msse  -msse2  -msse3  -mssse3  -msse4.1  -mpopcnt  -msse4.2  -mavx  -mf16c  -mfma  -mavx2  -mavx512f  -mavx512cd  -mavx512vl  -mavx512bw  -mavx512dq
  Extra  checks:  AVX512BW_MASK  AVX512DQ_MASK
  Detect  :  AVX512_SKX
  :  build/src.linux-x86_64-3.9/numpy/core/src/_simd/_simd.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_arithmetic.dispatch.c
  :  build/src.linux-x86_64-3.9/numpy/core/src/umath/loops_exponent_log.dispatch.c
CCompilerOpt.cache_flush[804]  :  write  cache  to  path  ->  /home/seiko/work/repos/numpy/build/temp.linux-x86_64-3.9/ccompiler_opt_cache_ext.py

########### CLIB COMPILER OPTIMIZATION ###########
Platform  :
  Architecture:  x64
  Compiler  :  gcc

CPU  baseline  :
  Requested  :  'min'
  Enabled  :  SSE  SSE2  SSE3
  Flags  :  -msse  -msse2  -msse3
  Extra  checks:  none

CPU  dispatch  :
  Requested  :  'max -xop -fma4'
  Enabled  :  SSSE3  SSE41  POPCNT  SSE42  AVX  F16C  FMA3  AVX2  AVX512F  AVX512CD  AVX512_KNL  AVX512_KNM  AVX512_SKX  AVX512_CLX  AVX512_CNL  AVX512_ICL
  Generated  :  none 

对于build_extbuild_clib中的每个部分都有一个单独的报告,每个部分都有几个值,表示以下内容:

平台:

  • 架构:目标 CPU 的架构名称。应该是x86x64ppc64ppc64learmhfaarch64s390xunknown中的一个。

  • 编译器:编译器名称。应该是 gcc、clang、msvc、icc、iccw 或类 Unix 的其中一个。

CPU 基线:

  • 请求:特定的--cpu-baseline功能和选项不变。

  • 已启用:最终启用的 CPU 功能集。

  • 标志:用于编译所有 NumPy C/C++源文件的编译器标志,除了用于生成分派功能的二进制对象的临时源文件。

  • 额外检查:激活与启用功能相关的某些功能或内部检查的列表,对于开发 SIMD 内核时进行调试非常有用。

CPU 分派:

  • 请求:特定的--cpu-dispatch功能和选项不变。

  • 已启用:最终启用的 CPU 功能集。

  • 生成的:在此属性的下一行开头,显示已生成优化的功能,以几个类似属性的部分形式显示,解释如下:

    • 一个或多个分派的功能:隐含的 CPU 功能。

    • 标志:用于这些功能的编译器标志。

    • 额外检查:类似于基线,但适用于这些分派功能。

    • 检测:需要在运行时检测的 CPU 功能集,以执行生成的优化。

    • 在上述属性之后以及以单独一行的‘:’结尾的行代表定义生成优化的 c/c++源文件的路径。

运行时分派

导入 NumPy 会触发从可分派功能集中扫描可用 CPU 功能。这可以通过将环境变量NPY_DISABLE_CPU_FEATURES设置为以逗号、制表符或空格分隔的功能列表来进一步限制。如果解析失败或未启用该功能,将引发错误。例如,在x86_64上,这将禁用AVX2FMA3

NPY_DISABLE_CPU_FEATURES="AVX2,FMA3" 

如果该功能不可用,将发出警告。

CPU 调度器是如何工作的?

原文:numpy.org/doc/1.26/reference/simd/how-it-works.html

NumPy 调度器基于多源编译,这意味着采用一定的源代码,并使用不同的编译器标志以及不同的C定义来多次进行编译,这些定义影响代码路径。这使得每个编译后的对象可以根据所需的优化启用某些指令集,并最终链接返回的对象在一起。

../../_images/opt-infra.png

这种机制应该支持所有编译器,并且不需要任何特定于编译器的扩展,但与此同时,它会对正常编译增加一些步骤,下面将对此进行解释。

1- 配置

在开始构建源文件之前,用户通过上述两个命令行参数配置所需的优化:

  • --cpu-baseline:所需优化的最小集合。

  • --cpu-dispatch:附加优化的调度集合。

2- 发现环境

在这一部分,我们检查编译器和平台架构,并缓存一些中间结果以加快重新构建的速度。

3- 验证所请求的优化

通过针对编译器进行测试,以及查看编译器根据请求的优化所支持的内容。

4- 生成主配置头文件

生成的头文件 _cpu_dispatch.h 包含了在前一步验证过的所需优化的指令集的所有定义和头文件。

它还包含了额外的 C 定义,用于定义 NumPy 的 Python 模块属性 __cpu_baseline____cpu_dispatch__

这个头文件中有什么内容?

此示例头文件是在 X86 机器上由 gcc 动态生成的。编译器支持--cpu-baseline="sse sse2 sse3"--cpu-dispatch="ssse3 sse41",结果如下。

// The header should be located at numpy/numpy/core/src/common/_cpu_dispatch.h
/**NOTE
 ** C definitions prefixed with "NPY_HAVE_" represent
 ** the required optimizations.
 **
 ** C definitions prefixed with 'NPY__CPU_TARGET_' are protected and
 ** shouldn't be used by any NumPy C sources.
 */
/******* baseline features *******/
/** SSE **/
#define NPY_HAVE_SSE 1
#include  <xmmintrin.h>
/** SSE2 **/
#define NPY_HAVE_SSE2 1
#include  <emmintrin.h>
/** SSE3 **/
#define NPY_HAVE_SSE3 1
#include  <pmmintrin.h>

/******* dispatch-able features *******/
#ifdef NPY__CPU_TARGET_SSSE3
  /** SSSE3 **/
  #define NPY_HAVE_SSSE3 1
  #include  <tmmintrin.h>
#endif
#ifdef NPY__CPU_TARGET_SSE41
  /** SSE41 **/
  #define NPY_HAVE_SSE41 1
  #include  <smmintrin.h>
#endif 

基线特性是通过--cpu-baseline配置的所需优化的最小集合。它们没有预处理保护,并且始终开启,这意味着它们可以在任何源代码中使用。

这是否意味着 NumPy 的基础设施将基线特性的编译器标志传递给所有源代码?

当然可以。但是可分发的源代码会被不同对待。

如果用户在构建过程中指定了特定的基线特性,但在运行时机器甚至不支持这些特性,会怎么样?编译后的代码是否会通过这些定义之一被调用,或者也许编译器本身基于提供的命令行编译器标志自动生成/矢量化某些代码片段?

在加载 NumPy 模块时,有一个验证步骤来检测这种行为。它会引发 Python 运行时错误以通知用户。这是为了防止 CPU 达到非法指令错误,导致段错误。

可调度特性 是我们通过 --cpu-dispatch 配置的一组附加优化。它们不会默认激活,并且始终由其他以 NPY__CPU_TARGET_ 为前缀的 C 定义保护。C 定义 NPY__CPU_TARGET_ 仅在 可调度源 内启用。

5- 可调度源和配置语句

可调度源是特殊的 C 文件,可以使用不同的编译器标志和不同的 C 定义进行多次编译。这些会影响代码路径,以便根据每个编译对象顶部必须声明的“配置语句”来启用某些指令集。同时,如果通过命令参数 --disable-optimization 禁用了优化,则可调度源将被视为普通的 C 源。

什么是配置语句?

配置语句是一种组合在一起以确定可调度源所需优化的关键字。

示例:

/*@targets avx2 avx512f vsx2 vsx3 asimd asimdhp */
// C code 

这些关键词主要代表了通过 --cpu-dispatch 配置的附加优化,但也可以代表其他选项,例如:

  • 目标组:用于管理可调度源文件外部所需优化的预配置配置语句。

  • 策略:一组选项,用于更改默认行为或强制编译器执行某些操作。

  • “baseline”:一个唯一的关键字,表示通过 --cpu-baseline 配置的最小优化。

Numpy 的基础设施处理可调度源有四个步骤

  • (A) 认知:就像源模板和 F2PY 一样,可调度的源文件需要一个特殊的扩展名 *.dispatch.c 来标记 C 可调度的源文件,而对于 C++ 则是 *.dispatch.cpp*.dispatch.cxx 注意:目前不支持 C++。

  • (B) 解析和验证:在此步骤中,先前通过上一步筛选的可调度源逐个解析和验证其配置语句,以确定所需的优化。

  • (C) 封装:这是 NumPy 基础设施采取的方法,已被证明足够灵活,可以使用不同的 C 定义和标志多次编译单个源,从而影响代码路径。该过程通过为与附加优化相关的每个所需优化创建临时 C 源来实现,其中包含 C 定义的声明,并通过 C 指令 #include 包含相关源。要了解更多细节,请查看以下 AVX512F 代码:

    /*
     * this definition is used by NumPy utilities as suffixes for the
     * exported symbols
     */
    #define NPY__CPU_TARGET_CURRENT AVX512F
    /*
     * The following definitions enable
     * definitions of the dispatch-able features that are defined within the main
     * configuration header. These are definitions for the implied features.
     */
    #define NPY__CPU_TARGET_SSE
    #define NPY__CPU_TARGET_SSE2
    #define NPY__CPU_TARGET_SSE3
    #define NPY__CPU_TARGET_SSSE3
    #define NPY__CPU_TARGET_SSE41
    #define NPY__CPU_TARGET_POPCNT
    #define NPY__CPU_TARGET_SSE42
    #define NPY__CPU_TARGET_AVX
    #define NPY__CPU_TARGET_F16C
    #define NPY__CPU_TARGET_FMA3
    #define NPY__CPU_TARGET_AVX2
    #define NPY__CPU_TARGET_AVX512F
    // our dispatch-able source
    #include  "/the/absuolate/path/of/hello.dispatch.c" 
    
  • (D)可调度配置头文件:基础设施为每个可调度源代码生成一个配置头文件,该头文件主要包含两个抽象的C宏,用于通过任何C源代码从生成的对象中调度特定的符号。它还用于前向声明。

    生成头文件以可调度源的名称命名,排除扩展名并替换为.h,例如假设我们有一个名为hello.dispatch.c的可调度源代码,其内容如下:

    // hello.dispatch.c
    /*@targets baseline sse42 avx512f */
    #include  <stdio.h>
    #include  "numpy/utils.h" // NPY_CAT, NPY_TOSTR
    
    #ifndef NPY__CPU_TARGET_CURRENT
      // wrapping the dispatch-able source only happens to the additional optimizations
      // but if the keyword 'baseline' provided within the configuration statements,
      // the infrastructure will add extra compiling for the dispatch-able source by
      // passing it as-is to the compiler without any changes.
      #define CURRENT_TARGET(X) X
      #define NPY__CPU_TARGET_CURRENT baseline // for printing only
    #else
      // since we reach to this point, that's mean we're dealing with
      // the additional optimizations, so it could be SSE42 or AVX512F
      #define CURRENT_TARGET(X) NPY_CAT(NPY_CAT(X, _), NPY__CPU_TARGET_CURRENT)
    #endif
    // Macro 'CURRENT_TARGET' adding the current target as suffux to the exported symbols,
    // to avoid linking duplications, NumPy already has a macro called
    // 'NPY_CPU_DISPATCH_CURFX' similar to it, located at
    // numpy/numpy/core/src/common/npy_cpu_dispatch.h
    // NOTE: we tend to not adding suffixes to the baseline exported symbols
    void  CURRENT_TARGET(simd_whoami)(const  char  *extra_info)
    {
      printf("I'm "  NPY_TOSTR(NPY__CPU_TARGET_CURRENT)  ", %s\n",  extra_info);
    } 
    

    现在假设您将hello.dispatch.c附加到源代码树上,那么基础设施应该生成一个名为hello.dispatch.h的临时配置头文件,任何源代码都可以访问它,它应该包含以下代码:

    #ifndef NPY__CPU_DISPATCH_EXPAND_
      // To expand the macro calls in this header
      #define NPY__CPU_DISPATCH_EXPAND_(X) X
    #endif
    // Undefining the following macros, due to the possibility of including config headers
    // multiple times within the same source and since each config header represents
    // different required optimizations according to the specified configuration
    // statements in the dispatch-able source that derived from it.
    #undef NPY__CPU_DISPATCH_BASELINE_CALL
    #undef NPY__CPU_DISPATCH_CALL
    // nothing strange here, just a normal preprocessor callback
    // enabled only if 'baseline' specified within the configuration statements
    #define NPY__CPU_DISPATCH_BASELINE_CALL(CB, ...) \
     NPY__CPU_DISPATCH_EXPAND_(CB(__VA_ARGS__))
    // 'NPY__CPU_DISPATCH_CALL' is an abstract macro is used for dispatching
    // the required optimizations that specified within the configuration statements.
    //
    // @param CHK, Expected a macro that can be used to detect CPU features
    // in runtime, which takes a CPU feature name without string quotes and
    // returns the testing result in a shape of boolean value.
    // NumPy already has macro called "NPY_CPU_HAVE", which fits this requirement.
    //
    // @param CB, a callback macro that expected to be called multiple times depending
    // on the required optimizations, the callback should receive the following arguments:
    //  1- The pending calls of @param CHK filled up with the required CPU features,
    //     that need to be tested first in runtime before executing call belong to
    //     the compiled object.
    //  2- The required optimization name, same as in 'NPY__CPU_TARGET_CURRENT'
    //  3- Extra arguments in the macro itself
    //
    // By default the callback calls are sorted depending on the highest interest
    // unless the policy "$keep_sort" was in place within the configuration statements
    // see "Dive into the CPU dispatcher" for more clarification.
    #define NPY__CPU_DISPATCH_CALL(CHK, CB, ...) \
     NPY__CPU_DISPATCH_EXPAND_(CB((CHK(AVX512F)), AVX512F, __VA_ARGS__)) \
     NPY__CPU_DISPATCH_EXPAND_(CB((CHK(SSE)&&CHK(SSE2)&&CHK(SSE3)&&CHK(SSSE3)&&CHK(SSE41)), SSE41, __VA_ARGS__)) 
    

    根据上述内容使用配置头文件的示例:

    // NOTE: The following macros are only defined for demonstration purposes only.
    // NumPy already has a collections of macros located at
    // numpy/numpy/core/src/common/npy_cpu_dispatch.h, that covers all dispatching
    // and declarations scenarios.
    
    #include  "numpy/npy_cpu_features.h" // NPY_CPU_HAVE
    #include  "numpy/utils.h" // NPY_CAT, NPY_EXPAND
    
    // An example for setting a macro that calls all the exported symbols at once
    // after checking if they're supported by the running machine.
    #define DISPATCH_CALL_ALL(FN, ARGS) \
     NPY__CPU_DISPATCH_CALL(NPY_CPU_HAVE, DISPATCH_CALL_ALL_CB, FN, ARGS) \
     NPY__CPU_DISPATCH_BASELINE_CALL(DISPATCH_CALL_BASELINE_ALL_CB, FN, ARGS)
    // The preprocessor callbacks.
    // The same suffixes as we define it in the dispatch-able source.
    #define DISPATCH_CALL_ALL_CB(CHECK, TARGET_NAME, FN, ARGS) \
     if (CHECK) { NPY_CAT(NPY_CAT(FN, _), TARGET_NAME) ARGS; }
    #define DISPATCH_CALL_BASELINE_ALL_CB(FN, ARGS) \
     FN NPY_EXPAND(ARGS);
    
    // An example for setting a macro that calls the exported symbols of highest
    // interest optimization, after checking if they're supported by the running machine.
    #define DISPATCH_CALL_HIGH(FN, ARGS) \
     if (0) {} \
     NPY__CPU_DISPATCH_CALL(NPY_CPU_HAVE, DISPATCH_CALL_HIGH_CB, FN, ARGS) \
     NPY__CPU_DISPATCH_BASELINE_CALL(DISPATCH_CALL_BASELINE_HIGH_CB, FN, ARGS)
    // The preprocessor callbacks
    // The same suffixes as we define it in the dispatch-able source.
    #define DISPATCH_CALL_HIGH_CB(CHECK, TARGET_NAME, FN, ARGS) \
     else if (CHECK) { NPY_CAT(NPY_CAT(FN, _), TARGET_NAME) ARGS; }
    #define DISPATCH_CALL_BASELINE_HIGH_CB(FN, ARGS) \
     else { FN NPY_EXPAND(ARGS); }
    
    // NumPy has a macro called 'NPY_CPU_DISPATCH_DECLARE' can be used
    // for forward declarations any kind of prototypes based on
    // 'NPY__CPU_DISPATCH_CALL' and 'NPY__CPU_DISPATCH_BASELINE_CALL'.
    // However in this example, we just handle it manually.
    void  simd_whoami(const  char  *extra_info);
    void  simd_whoami_AVX512F(const  char  *extra_info);
    void  simd_whoami_SSE41(const  char  *extra_info);
    
    void  trigger_me(void)
    {
      // bring the auto-generated config header
      // which contains config macros 'NPY__CPU_DISPATCH_CALL' and
      // 'NPY__CPU_DISPATCH_BASELINE_CALL'.
      // it is highly recommended to include the config header before executing
      // the dispatching macros in case if there's another header in the scope.
      #include  "hello.dispatch.h"
      DISPATCH_CALL_ALL(simd_whoami,  ("all"))
      DISPATCH_CALL_HIGH(simd_whoami,  ("the highest interest"))
      // An example of including multiple config headers in the same source
      // #include "hello2.dispatch.h"
      // DISPATCH_CALL_HIGH(another_function, ("the highest interest"))
    } 
    

1- 配置

在开始构建源文件之前,通过上述两个命令参数配置所需的优化:

  • --cpu-baseline: 最小集合的必需优化。

  • --cpu-dispatch: 分派的一组额外优化。

2- 发现环境

在此部分,我们检查编译器和平台架构,并缓存一些中间结果以加快重建速度。

3- 验证所请求的优化

通过对它们进行编译器测试,并根据所请求的优化查看编译器可以支持的内容。

4- 生成主配置头文件

生成的头文件_cpu_dispatch.h包含在上一步中验证的所需优化的所有定义和指令集的标头。

它还包含用于定义 NumPy 的 Python 级模块属性__cpu_baseline____cpu_dispatch__的额外 C 定义。

此标题中包含什么?

该示例标题在一个 X86 机器上由 gcc 动态生成。编译器支持--cpu-baseline="sse sse2 sse3"--cpu-dispatch="ssse3 sse41",结果如下。

// The header should be located at numpy/numpy/core/src/common/_cpu_dispatch.h
/**NOTE
 ** C definitions prefixed with "NPY_HAVE_" represent
 ** the required optimizations.
 **
 ** C definitions prefixed with 'NPY__CPU_TARGET_' are protected and
 ** shouldn't be used by any NumPy C sources.
 */
/******* baseline features *******/
/** SSE **/
#define NPY_HAVE_SSE 1
#include  <xmmintrin.h>
/** SSE2 **/
#define NPY_HAVE_SSE2 1
#include  <emmintrin.h>
/** SSE3 **/
#define NPY_HAVE_SSE3 1
#include  <pmmintrin.h>

/******* dispatch-able features *******/
#ifdef NPY__CPU_TARGET_SSSE3
  /** SSSE3 **/
  #define NPY_HAVE_SSSE3 1
  #include  <tmmintrin.h>
#endif
#ifdef NPY__CPU_TARGET_SSE41
  /** SSE41 **/
  #define NPY_HAVE_SSE41 1
  #include  <smmintrin.h>
#endif 

基线特性是通过--cpu-baseline配置的最小集合的所需优化。它们没有预处理器保护,并且始终启用,这意味着它们可以在任何源代码中使用。

这是否意味着 NumPy 的基础设施将编译器的基线特性标志传递给所有源代码?

当然,是的。但是可调度源代码会被另外处理。

如果用户在构建过程中指定了某些基线特性,但在运行时机器甚至不支持这些特性怎么办?编译后的代码是否会通过这些定义之一调用,或者编译器是否会根据提供的命令行编译器标志自动生成/矢量化某段代码?

在加载 NumPy 模块期间,会发现这种行为的验证步骤。它会引发 Python 运行时错误通知用户。这是为了防止 CPU 达到非法指令错误而导致段错误。

分发功能是我们通过--cpu-dispatch配置的分发的一组额外优化。它们不会默认激活,并始终由以NPY__CPU_TARGET_为前缀的其他 C 定义保护。C 定义NPY__CPU_TARGET_仅在分发源内启用。

5- 分发源和配置语句

分发源是特殊的C文件,可以使用不同的编译器标志和不同的C定义多次编译。这些影响代码路径,根据“配置语句”启用每个编译对象的某些指令集,这些语句必须在C注释(/**/)中声明,并在每个分发源顶部以特殊标记@targets开头。同时,如果通过命令参数--disable-optimization禁用优化,则将分发源视为正常的C源。

什么是配置语句?

配置语句是一种关键字的组合,用于确定分发源所需的优化。

例子:

/*@targets avx2 avx512f vsx2 vsx3 asimd asimdhp */
// C code 

这些关键字主要代表通过--cpu-dispatch配置的额外优化,但也可以代表其他选项,如:

  • 目标组:用于管理分发源外部所需优化的预配置配置语句。

  • 策略:用于改变默认行为或强制编译器执行某些操作的选项集合。

  • “baseline”:一个独特的关键字,代表通过--cpu-baseline配置的最小优化

NumPy 的基础结构处理分发源需要经过四个步骤

  • (A) 识别:就像源模板和 F2PY 一样,分发源文件需要一个特殊的扩展名*.dispatch.c来标记 C 分发源文件,对于 C++为*.dispatch.cpp*.dispatch.cxx 注意:C++目前不受支持。

  • (B) 解析和验证:在这个步骤中,通过上一步筛选的分发源将按顺序分别由配置语句解析和验证,以确定所需的优化。

  • (C) 封装:这是 NumPy 基础设施采用的方法,已经证明足够灵活,可以编译多次相同的源文件,但使用不同的 C 定义和影响代码路径的标志。该过程通过为与额外优化相关的每个必需的优化创建临时 C 源文件来实现,其中包含 C 定义的声明,并通过 C 指令 #include 包含相关源文件。为了更好地说明,请看下面的 AVX512F 代码:

    /*
     * this definition is used by NumPy utilities as suffixes for the
     * exported symbols
     */
    #define NPY__CPU_TARGET_CURRENT AVX512F
    /*
     * The following definitions enable
     * definitions of the dispatch-able features that are defined within the main
     * configuration header. These are definitions for the implied features.
     */
    #define NPY__CPU_TARGET_SSE
    #define NPY__CPU_TARGET_SSE2
    #define NPY__CPU_TARGET_SSE3
    #define NPY__CPU_TARGET_SSSE3
    #define NPY__CPU_TARGET_SSE41
    #define NPY__CPU_TARGET_POPCNT
    #define NPY__CPU_TARGET_SSE42
    #define NPY__CPU_TARGET_AVX
    #define NPY__CPU_TARGET_F16C
    #define NPY__CPU_TARGET_FMA3
    #define NPY__CPU_TARGET_AVX2
    #define NPY__CPU_TARGET_AVX512F
    // our dispatch-able source
    #include  "/the/absuolate/path/of/hello.dispatch.c" 
    
  • (D) 可调度配置头文件:基础设施为每个可调度源生成一个配置头文件,该头文件主要包含两个抽象的 C 宏,用于标识生成的对象,以便可以在运行时通过任何 C 源代码调度从生成的对象中的某些符号。它也用于前向声明。

    生成的头文件采用可调度源的名称,排除扩展名,并替换为.h,例如假设我们有一个名为 hello.dispatch.c 的可调度源,并包含以下内容:

    // hello.dispatch.c
    /*@targets baseline sse42 avx512f */
    #include  <stdio.h>
    #include  "numpy/utils.h" // NPY_CAT, NPY_TOSTR
    
    #ifndef NPY__CPU_TARGET_CURRENT
      // wrapping the dispatch-able source only happens to the additional optimizations
      // but if the keyword 'baseline' provided within the configuration statements,
      // the infrastructure will add extra compiling for the dispatch-able source by
      // passing it as-is to the compiler without any changes.
      #define CURRENT_TARGET(X) X
      #define NPY__CPU_TARGET_CURRENT baseline // for printing only
    #else
      // since we reach to this point, that's mean we're dealing with
      // the additional optimizations, so it could be SSE42 or AVX512F
      #define CURRENT_TARGET(X) NPY_CAT(NPY_CAT(X, _), NPY__CPU_TARGET_CURRENT)
    #endif
    // Macro 'CURRENT_TARGET' adding the current target as suffux to the exported symbols,
    // to avoid linking duplications, NumPy already has a macro called
    // 'NPY_CPU_DISPATCH_CURFX' similar to it, located at
    // numpy/numpy/core/src/common/npy_cpu_dispatch.h
    // NOTE: we tend to not adding suffixes to the baseline exported symbols
    void  CURRENT_TARGET(simd_whoami)(const  char  *extra_info)
    {
      printf("I'm "  NPY_TOSTR(NPY__CPU_TARGET_CURRENT)  ", %s\n",  extra_info);
    } 
    

    假设你已经将 hello.dispatch.c 附加到源树中,那么基础设施应该生成一个临时的配置头文件,名为 hello.dispatch.h,可以被源树中的任何源文件访问,并且应包含以下代码:

    #ifndef NPY__CPU_DISPATCH_EXPAND_
      // To expand the macro calls in this header
      #define NPY__CPU_DISPATCH_EXPAND_(X) X
    #endif
    // Undefining the following macros, due to the possibility of including config headers
    // multiple times within the same source and since each config header represents
    // different required optimizations according to the specified configuration
    // statements in the dispatch-able source that derived from it.
    #undef NPY__CPU_DISPATCH_BASELINE_CALL
    #undef NPY__CPU_DISPATCH_CALL
    // nothing strange here, just a normal preprocessor callback
    // enabled only if 'baseline' specified within the configuration statements
    #define NPY__CPU_DISPATCH_BASELINE_CALL(CB, ...) \
     NPY__CPU_DISPATCH_EXPAND_(CB(__VA_ARGS__))
    // 'NPY__CPU_DISPATCH_CALL' is an abstract macro is used for dispatching
    // the required optimizations that specified within the configuration statements.
    //
    // @param CHK, Expected a macro that can be used to detect CPU features
    // in runtime, which takes a CPU feature name without string quotes and
    // returns the testing result in a shape of boolean value.
    // NumPy already has macro called "NPY_CPU_HAVE", which fits this requirement.
    //
    // @param CB, a callback macro that expected to be called multiple times depending
    // on the required optimizations, the callback should receive the following arguments:
    //  1- The pending calls of @param CHK filled up with the required CPU features,
    //     that need to be tested first in runtime before executing call belong to
    //     the compiled object.
    //  2- The required optimization name, same as in 'NPY__CPU_TARGET_CURRENT'
    //  3- Extra arguments in the macro itself
    //
    // By default the callback calls are sorted depending on the highest interest
    // unless the policy "$keep_sort" was in place within the configuration statements
    // see "Dive into the CPU dispatcher" for more clarification.
    #define NPY__CPU_DISPATCH_CALL(CHK, CB, ...) \
     NPY__CPU_DISPATCH_EXPAND_(CB((CHK(AVX512F)), AVX512F, __VA_ARGS__)) \
     NPY__CPU_DISPATCH_EXPAND_(CB((CHK(SSE)&&CHK(SSE2)&&CHK(SSE3)&&CHK(SSSE3)&&CHK(SSE41)), SSE41, __VA_ARGS__)) 
    

    根据上述示例使用配置头文件的示例:

    // NOTE: The following macros are only defined for demonstration purposes only.
    // NumPy already has a collections of macros located at
    // numpy/numpy/core/src/common/npy_cpu_dispatch.h, that covers all dispatching
    // and declarations scenarios.
    
    #include  "numpy/npy_cpu_features.h" // NPY_CPU_HAVE
    #include  "numpy/utils.h" // NPY_CAT, NPY_EXPAND
    
    // An example for setting a macro that calls all the exported symbols at once
    // after checking if they're supported by the running machine.
    #define DISPATCH_CALL_ALL(FN, ARGS) \
     NPY__CPU_DISPATCH_CALL(NPY_CPU_HAVE, DISPATCH_CALL_ALL_CB, FN, ARGS) \
     NPY__CPU_DISPATCH_BASELINE_CALL(DISPATCH_CALL_BASELINE_ALL_CB, FN, ARGS)
    // The preprocessor callbacks.
    // The same suffixes as we define it in the dispatch-able source.
    #define DISPATCH_CALL_ALL_CB(CHECK, TARGET_NAME, FN, ARGS) \
     if (CHECK) { NPY_CAT(NPY_CAT(FN, _), TARGET_NAME) ARGS; }
    #define DISPATCH_CALL_BASELINE_ALL_CB(FN, ARGS) \
     FN NPY_EXPAND(ARGS);
    
    // An example for setting a macro that calls the exported symbols of highest
    // interest optimization, after checking if they're supported by the running machine.
    #define DISPATCH_CALL_HIGH(FN, ARGS) \
     if (0) {} \
     NPY__CPU_DISPATCH_CALL(NPY_CPU_HAVE, DISPATCH_CALL_HIGH_CB, FN, ARGS) \
     NPY__CPU_DISPATCH_BASELINE_CALL(DISPATCH_CALL_BASELINE_HIGH_CB, FN, ARGS)
    // The preprocessor callbacks
    // The same suffixes as we define it in the dispatch-able source.
    #define DISPATCH_CALL_HIGH_CB(CHECK, TARGET_NAME, FN, ARGS) \
     else if (CHECK) { NPY_CAT(NPY_CAT(FN, _), TARGET_NAME) ARGS; }
    #define DISPATCH_CALL_BASELINE_HIGH_CB(FN, ARGS) \
     else { FN NPY_EXPAND(ARGS); }
    
    // NumPy has a macro called 'NPY_CPU_DISPATCH_DECLARE' can be used
    // for forward declarations any kind of prototypes based on
    // 'NPY__CPU_DISPATCH_CALL' and 'NPY__CPU_DISPATCH_BASELINE_CALL'.
    // However in this example, we just handle it manually.
    void  simd_whoami(const  char  *extra_info);
    void  simd_whoami_AVX512F(const  char  *extra_info);
    void  simd_whoami_SSE41(const  char  *extra_info);
    
    void  trigger_me(void)
    {
      // bring the auto-generated config header
      // which contains config macros 'NPY__CPU_DISPATCH_CALL' and
      // 'NPY__CPU_DISPATCH_BASELINE_CALL'.
      // it is highly recommended to include the config header before executing
      // the dispatching macros in case if there's another header in the scope.
      #include  "hello.dispatch.h"
      DISPATCH_CALL_ALL(simd_whoami,  ("all"))
      DISPATCH_CALL_HIGH(simd_whoami,  ("the highest interest"))
      // An example of including multiple config headers in the same source
      // #include "hello2.dispatch.h"
      // DISPATCH_CALL_HIGH(another_function, ("the highest interest"))
    } 
    

NumPy 安全性

原文:numpy.org/doc/1.26/reference/security.html

安全问题可以按照项目 README 中描述的方式进行私下报告,也可以在打开问题跟踪器上的新问题时进行报告。Python 安全报告指南是一个很好的资源,其中的注意事项也适用于 NumPy。

NumPy 的维护人员不是安全专家。然而,我们对安全问题持有责任感,对 NumPy 代码库及其使用方式的专家。在发布针对 NumPy 的安全公告之前,请通知我们,因为我们乐意优先处理问题或帮助评估错误的严重性。我们事先不知道的安全公告可能会给所有相关方带来大量工作。

在处理不可信数据时的建议使用 NumPy

可以自由执行 NumPy(或 Python)函数的用户必须被视为具有相同特权的进程/Python 解释器。

也就是说,NumPy 通常能够安全使用来自非特权用户的数据,并且通过安全的 API 函数读取(例如从文本文件或没有 pickle 支持的.npy文件加载)。恶意数据大小不应导致特权升级。

使用不受信任数据时,以下要点可能很有用或应该注意:

  • 耗尽内存可能导致内存溢出杀死,这是可能的拒绝服务攻击。可能的原因包括:

    • 读取文本文件的函数可能需要比原始输入文件大小多得多的内存。

    • 如果用户可以创建任意形状的数组,NumPy 的广播意味着中间或结果数组可能比输入大得多。

  • NumPy 结构化 dtype 允许大量的复杂性。幸运的是,当提供意外的结构化 dtype 时,大多数代码会出现优雅失效。然而,代码应该禁止不受信任的用户提供这些(例如通过.npy文件)或仔细检查包含的字段以防止嵌套结构/子数组 dtype。

  • 通常应考虑通过用户输入传递是不安全的(除了被读取的数据)。例如,np.dtype(user_string)dtype=user_string

  • 操作的速度可能取决于值和内存顺序可能导致更大的临时内存使用和较慢的执行。这意味着操作可能比简单测试用例显着慢或使用更多内存。

  • 在读取数据时,考虑强制规定特定形状(例如一维)或 dtype,如float64float32int64,以减少复杂性。

当处理非常规不可信数据时,建议对分析进行沙盒化,以防潜在的特权升级。如果使用基于 NumPy 的进一步库,则特别建议这样做,因为这些库会增加额外的复杂性和潜在的安全问题。

在处理不受信任数据时的建议使用 NumPy

能够自由执行 NumPy(或 Python)函数的用户必须被视为拥有与进程/Python 解释器相同的特权。

也就是说,NumPy 应该通常是安全的,可用于由特权用户提供的数据,并通过安全的 API 函数读取(例如从文本文件或不支持 pickle 的 .npy 文件加载)。恶意的数据大小绝不应导致特权升级。

在处理不受信任的数据时,以下几点可能有用或值得注意:

  • 耗尽内存可能导致内存溢出,这是一种可能的拒绝服务攻击。可能的原因包括:

    • 读取文本文件的函数可能需要比原始输入文件大得多的内存。

    • 如果用户可以创建任意形状的数组,NumPy 的广播意味着中间或结果数组可能远大于输入。

  • NumPy 结构化 dtypes 允许大量复杂性。幸运的是,当提供意外的结构化 dtype 时,大多数代码都能优雅地失败。然而,代码应该禁止不受信任的用户提供这些(例如通过 .npy 文件),或仔细检查包含的嵌套结构化/子数组 dtypes 的字段。

  • 将用户输入传递下去通常应被视为不安全的(读取的数据除外)。一个例子是 np.dtype(user_string)dtype=user_string

  • 操作的速度可能取决于值,内存顺序可能导致更大的临时内存使用和更慢的执行。这意味着与简单的测试用例相比,操作可能显著更慢或使用更多内存。

  • 在读取数据时,考虑强制执行特定的形状(例如一维)或 dtype,如 float64float32int64,以减少复杂性。

在处理非平凡的不受信任数据时,建议对分析进行沙箱化以防止潜在的特权升级。如果进一步使用基于 NumPy 的库,这尤其是个好主意,因为这些库增加了额外的复杂性和潜在的安全问题。

NumPy 和 SWIG

原文:numpy.org/doc/1.26/reference/swig.html

  • numpy.i:用于 NumPy 的 SWIG 接口文件

    • 介绍

    • 使用 numpy.i

    • 可用的类型映射

    • NumPy 数组标量和 SWIG

    • 辅助函数

    • 超越提供的类型映射

    • 总结

  • 测试 numpy.i 类型映射

    • 介绍

    • 测试组织

    • 测试头文件

    • 测试源文件

    • 测试 SWIG 接口文件

    • 测试 Python 脚本

numpy.i:NumPy 的 SWIG 接口文件

原文:numpy.org/doc/1.26/reference/swig.interface-file.html

简介

用于生成适用于各种脚本语言的包装器代码的强大工具——简单包装器和接口生成器(或SWIG)。SWIG可以解析头文件,并仅使用代码原型,为目标语言创建接口。但SWIG并非无所不能。例如,它无法通过原型了解到:

double rms(double* seq, int n); 

seq到底是什么。它是一个要原地更改的单个值吗?它是一个数组,如果是的话,它的长度是多少?它只能输入吗?只能输出?输入输出?SWIG无法确定这些细节,并且也不试图这样做。

如果我们设计了rms,那么我们可能使其成为一个仅接受名为seqdouble值长度为n的输入数组的例程,并返回均方根。然而,SWIG的默认行为将是创建一个可编译但几乎无法像 C 例程预期的那样从脚本语言使用的包装器函数。

对于 Python,处理连续(或技术上的跨度)块的同质数据的首选方式是使用 NumPy,它提供对数据的多维数组的完整面向对象访问。因此,rms函数的最合理 Python 接口应该是(包括文档字符串):

def rms(seq):
  """
 rms: return the root mean square of a sequence
 rms(numpy.ndarray) -> double
 rms(list) -> double
 rms(tuple) -> double
 """ 

其中seq将是double值的 NumPy 数组,并且它的长度n将在传递给 C 例程之前从内部seq中提取。更好的是,由于 NumPy 支持从任意 Python 序列构建数组,seq本身可以是几乎任意序列(只要每个元素都可以转换为double),包装器代码会在提取数据和长度之前将其内部转换为 NumPy 数组。

SWIG允许通过一种称为typemaps的机制定义这些类型的转换。本文为您提供了如何使用numpy.i,一个SWIG接口文件的信息,该文件定义了一系列 typemaps,旨在使上面描述的与数组相关的转换类型相对简单地实现。例如,假设上面定义的rms函数原型位于名为rms.h的头文件中。要获得上述讨论的 Python 接口,您的SWIG接口文件将需要以下内容:

%{
#define SWIG_FILE_WITH_INIT
#include "rms.h"
%}

%include "numpy.i"

%init %{
import_array();
%}

%apply (double* IN_ARRAY1, int DIM1) {(double* seq, int n)};
%include "rms.h" 

Typemaps 是基于一个或多个函数参数列表进行关键化,可以是类型或类型和名称。我们将这些列表称为签名numpy.i定义的众多 typemap 之一被上述使用,具有签名(double* IN_ARRAY1, int DIM1)。参数名意在表明double*参数是一个一维输入数组,int代表该维度的大小。这正是rms原型中的模式。

大部分情况下,将要包装的实际原型不会有参数名为IN_ARRAY1DIM1。我们使用SWIG%apply指令将一维double类型的输入数组的 typemap 应用到rms实际原型上。因此,有效地使用numpy.i需要知道可用的 typemap 以及它们的作用。

一个包含上述SWIG指令的SWIG接口文件将生成类似如下的包装器代码:

 1 PyObject *_wrap_rms(PyObject *args) {
 2   PyObject *resultobj = 0;
 3   double *arg1 = (double *) 0 ;
 4   int arg2 ;
 5   double result;
 6   PyArrayObject *array1 = NULL ;
 7   int is_new_object1 = 0 ;
 8   PyObject * obj0 = 0 ;
 9
10   if (!PyArg_ParseTuple(args,(char *)"O:rms",&obj0)) SWIG_fail;
11   {
12     array1 = obj_to_array_contiguous_allow_conversion(
13                  obj0, NPY_DOUBLE, &is_new_object1);
14     npy_intp size[1] = {
15       -1
16     };
17     if (!array1 || !require_dimensions(array1, 1) ||
18         !require_size(array1, size, 1)) SWIG_fail;
19     arg1 = (double*) array1->data;
20     arg2 = (int) array1->dimensions[0];
21   }
22   result = (double)rms(arg1,arg2);
23   resultobj = SWIG_From_double((double)(result));
24   {
25     if (is_new_object1 && array1) Py_DECREF(array1);
26   }
27   return resultobj;
28 fail:
29   {
30     if (is_new_object1 && array1) Py_DECREF(array1);
31   }
32   return NULL;
33 } 

numpy.i中的 typemaps 负责以下代码行:12–20,25 和 30。第 10 行解析了传递给rms函数的输入。从格式字符串"O:rms",我们可以看到参数列表预期是一个单一的 Python 对象(由冒号前的O指定),它的指针存储在obj0中。许多由numpy.i提供的函数被调用以完成并验证从通用 Python 对象到 NumPy 数组的(可能的)转换。这些函数在 Helper Functions 部分中有解释,但希望它们的名称是不言自明的。在第 12 行,我们使用obj0构造一个 NumPy 数组。在第 17 行,我们检查结果的有效性:非空且具有任意长度的单一维度。一旦这些状态验证通过,我们在第 19 和 20 行提取数据缓冲区和长度,以便在第 22 行调用底层 C 函数。第 25 行处理了在创建一个不再需要的新数组时的内存管理。

这段代码包含大量的错误处理。请注意,SWIG_fail是一个宏,用于引用第 28 行处的标签goto fail。如果用户提供了错误数量的参数,它会在第 10 行捕捉到。如果构建 NumPy 数组失败或生成具有错误维度数量的数组,则在第 17 行捕捉到这些错误。最后,如果检测到错误,则在第 30 行仍管理内存。

请注意,如果 C 函数签名的顺序不同:

double rms(int n, double* seq); 

那么SWIG将无法与给定rms参数列表匹配上述 typemap 签名。幸运的是,numpy.i有一组 typemap,其中数据指针在最后:

%apply (int DIM1, double* IN_ARRAY1) {(int n, double* seq)}; 

这只是将生成代码中第 3 和第 4 行中的arg1arg2的定义位置互换的效果,并且将它们在第 19 和 20 行的赋值位置也互换。

使用 numpy.i

目前,numpy.i 文件位于 numpy 安装目录下的 tools/swig 子目录中。通常,您会希望将其复制到您开发包装器的目录中。

一个只使用单个 SWIG 接口文件的简单模块应包括以下内容:

%{
#define SWIG_FILE_WITH_INIT
%}
%include "numpy.i"
%init %{
import_array();
%} 

在编译的 Python 模块中,import_array() 只应调用一次。这可能在你编写并链接到该模块的 C/C++ 文件中。如果是这种情况,那么你的任何接口文件都不应该 #define SWIG_FILE_WITH_INIT 或调用 import_array()。或者,这个初始化调用可能在由 SWIG 从具有上述 %init 块的接口文件生成的包装文件中。如果是这种情况,并且你有多个 SWIG 接口文件,则只有一个接口文件应该 #define SWIG_FILE_WITH_INIT 并调用 import_array()

可用的类型映射

numpy.i 提供的针对不同数据类型(如 双精度浮点型整型)和维度不同类型(如 整型长整型)的数组���类型映射指令,除了 C 和 NumPy 类型规格外,是相同的。因此,这些类型映射是通过宏实现的(通常是在幕后):

%numpy_typemaps(DATA_TYPE, DATA_TYPECODE, DIM_TYPE) 

可以为适当的 (数据类型, 数据类型代码, DIM_TYPE) 组合调用。例如:

%numpy_typemaps(double, NPY_DOUBLE, int)
%numpy_typemaps(int,    NPY_INT   , int) 

numpy.i 接口文件使用 %numpy_typemaps 宏来为以下 C 数据类型和 int 维度类型实现类型映射:

  • 有符号字符
  • 无符号字符
  • 短整型
  • 无符号短整型
  • 整型
  • 无符号整型
  • 长整型
  • 无符号长整型
  • 长长整型
  • 无符号长长整型
  • 单精度浮点型
  • 双精度浮点型

在以下描述中,我们引用了一个通用的 数据类型,它可以是上面列出的任何 C 数据类型之一,以及 DIM_TYPE,应该是多种类型中的其中一种。

类型映射签名在缓冲指针的名称上有很大的不同。带有 FARRAY 的名称用于 Fortran 排序数组,而带有 ARRAY 的名称用于 C 排序(或 1D 数组)。

输入数组

输入数组被定义为传入例程但不在原地更改或返回给用户的数据数组。因此,Python 输入数组几乎可以是任何可以转换为请求类型的数组的 Python 序列(如列表)。输入数组签名为

1D:

  • ( 数据类型 IN_ARRAY1[任意] )
  • ( 数据类型* IN_ARRAY1, int DIM1 )
  • ( int DIM1, 数据类型* IN_ARRAY1 )

2D:

  • ( 数据类型 IN_ARRAY2[任意][任意] )
  • ( 数据类型* IN_ARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, 数据类型* IN_ARRAY2 )
  • ( 数据类型* IN_FARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, 数据类型* IN_FARRAY2 )

3D:

  • ( 数据类型 IN_ARRAY3[任意][任意][任意] )
  • ( 数据类型* IN_ARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, 数据类型* IN_ARRAY3 )
  • ( 数据类型* IN_FARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, 数据类型* IN_FARRAY3 )

4D:

  • (数据类型 IN_ARRAY4[任意][任意][任意][任意])
  • (数据类型* IN_ARRAY4, 维度类型 维度 1, 维度类型 维度 2, 维度类型 维度 3, 维度类型 维度 4)
  • (维度类型 维度 1, 维度类型 维度 2, 维度类型 维度 3, , 维度类型 维���4, 数据类型* IN_ARRAY4)
  • (数据类型* IN_FARRAY4, 维度类型 维度 1, 维度类型 维度 2, 维度类型 维度 3, 维度类型 维度 4)
  • (维度类型 维度 1, 维度类型 维度 2, 维度类型 维度 3, 维度类型 维度 4, 数据类型* IN_FARRAY4)

所列的第一个签名 ( 数据类型 IN_ARRAY[任意] ) 是用于具有硬编码维度的一维数组。同样,( 数据类型 IN_ARRAY2[任意][任意] ) 用于具有硬编码维度的二维数组,以此类推和三维数组。

就地数组

就地数组被定义为原地修改的数组。输入值可能会被使用,也可能不会被使用,但函数返回时的值是重要的。因此,提供的 Python 参数必须是所需类型的 NumPy 数组。就地签名为

1D:

  • ( 数据类型 INPLACE_ARRAY1[任意] )
  • ( 数据类型* INPLACE_ARRAY1, int 维度 1 )
  • ( int 维度 1, 数据类型* INPLACE_ARRAY1 )

2D:

  • ( 数据类型 INPLACE_ARRAY2[任意][任意] )
  • ( 数据类型* INPLACE_ARRAY2, int 维度 1, int 维度 2 )
  • ( int 维度 1, int 维度 2, 数据类型* INPLACE_ARRAY2 )
  • ( 数据类型* INPLACE_FARRAY2, int 维度 1, int 维度 2 )
  • ( int 维度 1, int 维度 2, 数据类型* INPLACE_FARRAY2 )

3D:

  • ( 数据类型 INPLACE_ARRAY3[任意][任意][任意] )
  • ( 数据类型* INPLACE_ARRAY3, int 维度 1, int 维度 2, int 维度 3 )
  • ( int 维度 1, int 维度 2, int 维度 3, 数据类型* INPLACE_ARRAY3 )
  • ( 数据类型* INPLACE_FARRAY3, int 维度 1, int 维度 2, int 维度 3 )
  • ( int 维度 1, int 维度 2, int 维度 3, 数据类型* INPLACE_FARRAY3 )

4D:

  • (数据类型 INPLACE_ARRAY4[任意][任意][任意][任意])
  • (数据类型* INPLACE_ARRAY4, 维度类型 维度 1, 维度类型 维度 2, 维度类型 维度 3, 维度类型 维度 4)
  • (维度类型 维度 1, 维度类型 维度 2, 维度类型 维度 3, , 维度类型 维度 4, 数据类型* INPLACE_ARRAY4)
  • (数据类型* INPLACE_FARRAY4, 维度类型 维度 1, 维度类型 维度 2, 维度类型 维度 3, 维度类型 维度 4)
  • (维度类型 维度 1, 维度类型 维度 2, 维度类型 维度 3, 维度类型 维度 4, 数据类型* INPLACE_FARRAY4)

这些类型映射现在会检查INPLACE_ARRAY参数是否使用本机字节顺序。如果不是,则会引发异常。

还有一种“flat”就地数组,适用于无论维度如何都想修改或处理每个元素的情况。一个例子是“量化”函数,在此函数中,对数组的每个元素进行原地量化处理,无论是 1D、2D 还是其他。此形式会检查连续性,但允许 C 或 Fortran 排序。

ND:

  • (数据类型* INPLACE_ARRAY_FLAT, 维度类型 维度 FLAT)

输出数组

Argout 数组是在 C 中作为输入参数出现的数组,但实际上是输出数组。这种模式经常在存在多个输出变量且单个返回参数因此不足够时发生。在 Python 中,返回多个参数的常规方法是将它们打包到一个序列(元组、列表等)中并返回该序列。这就是 argout 类型映射的作用。如果使用这些 argout 类型映射的包装函数具有多个返回参数,则它们将被打包到一个元组或列表中,具体取决于 Python 的版本。Python 用户不需要传入这些数组,它们只会被返回。对于指定维度的情况,Python 用户必须将该维度作为参数提供。argout 签名为

1D:

  • (DATA_TYPE ARGOUT_ARRAY1[ANY])
  • (DATA_TYPE* ARGOUT_ARRAY1,int DIM1)
  • (int DIM1,DATA_TYPE* ARGOUT_ARRAY1)

2D:

  • (DATA_TYPE ARGOUT_ARRAY2[ANY][ANY])

3D:

  • (DATA_TYPE ARGOUT_ARRAY3[ANY][ANY][ANY])

4D:

  • (DATA_TYPE ARGOUT_ARRAY4[ANY][ANY][ANY][ANY])

这些通常用于在 C/C++ 中分配堆上的数组,并调用函数来填充数组的值的情况。在 Python 中,这些数组会为您分配并作为新数组对象返回。

注意,我们支持 1D 中的 DATA_TYPE* argout 类型映射,但不支持 2D 或 3D。这是由于 SWIG 类型映射语法的一个怪癖,无法避免。请注意,对于这些类型的 1D 类型映射,Python 函数将接受表示 DIM1 的单个参数。

Argout 视图数组

Argoutview 数组用于在 C 代码中提供内部数据视图而不需要用户分配任何内存的情况。这可能是危险的。几乎没有办法保证 C 代码的内部数据在封装它的 NumPy 数组的整个生命周期内都保持存在。如果用户在销毁提供数据视图的对象之前销毁了 NumPy 数组,那么使用该数组可能导致错误的内存引用或分段错误。尽管如此,在处理大型数据集的情况下,有时您别无选择。

要包装 Argoutview 数组的 C 代码的特征是指针:指向维度和指向数据的双指针,以便这些值可以回传给用户。因此,argoutview 类型映射的签名为

1D:

  • (数据类型** ARGOUTVIEW_ARRAY1,DIM_TYPE* DIM1)
  • (DIM_TYPE* DIM1,DATA_TYPE** ARGOUTVIEW_ARRAY1)

2D:

  • (DATA_TYPE** ARGOUTVIEW_ARRAY2,DIM_TYPE* DIM1,DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1,DIM_TYPE* DIM2,DATA_TYPE** ARGOUTVIEW_ARRAY2)
  • (DATA_TYPE** ARGOUTVIEW_FARRAY2,DIM_TYPE* DIM1,DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1,DIM_TYPE* DIM2,DATA_TYPE** ARGOUTVIEW_FARRAY2)

3D:

  • (DATA_TYPE** ARGOUTVIEW_ARRAY3,DIM_TYPE* DIM1,DIM_TYPE* DIM2,DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1,DIM_TYPE* DIM2,DIM_TYPE* DIM3,DATA_TYPE** ARGOUTVIEW_ARRAY3)
  • ( DATA_TYPE** ARGOUTVIEW_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEW_FARRAY3)

4D:

  • (DATA_TYPE** ARGOUTVIEW_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_ARRAY4)
  • (DATA_TYPE** ARGOUTVIEW_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_FARRAY4)

请注意,不支持具有硬编码维度的数组。这些无法遵循这些类型映射的双指针签名。

内存管理 Argout 视图数组

numpy.i 的最新补充是允许 argout 数组使用受管理内存视图的类型映射。请参见此处的讨论。

1D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY1, DIM_TYPE* DIM1)
  • (DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEWM_ARRAY1)

2D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_ARRAY2)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_FARRAY2)

3D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_ARRAY3)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_FARRAY3)

4D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_ARRAY4)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_FARRAY4)

输出数组

numpy.i 接口文件不支持输出数组的类型映射,原因有几个。首先,C/C++ 返回参数限制为单个值。这阻碍了以通用方式获取维度信息。其次,不允许作为返回参数使用硬编码长度的数组。换句话说:

double[3] newVector(double x, double y, double z); 

这不是合法的 C/C++ 语法。因此,我们无法提供以下形式的类型映射:

%typemap(out) (TYPE[ANY]); 

如果您遇到一个函数或方法返回指向数组的指针的情况,您最好的选择是编写自己的版本的函数来包装,可以是使用 %extend 对于类方法的情况,或使用 %ignore%rename 对于函数的情况。

其他常见类型: bool

请注意,C++ 类型 bool 在可用类型映射部分的列表中不受支持。NumPy 布尔值是单个字节,而 C++ bool 是四个字节(至少在我的系统上是这样)。因此:

%numpy_typemaps(bool, NPY_BOOL, int) 

会导致产生引用不正确数据长度的代码的 typemaps。你可以实现以下的宏展开:

%numpy_typemaps(bool, NPY_UINT, int) 

为了解决数据长度的问题,输入数组会很好地运作,但是原地数组可能会失败类型检查。

其他常见类型:复杂

对于复杂浮点类型的 typemap 转换也不会自动支持。这是因为 Python 和 NumPy 是用 C 编写的,而 C 没有原生的复杂类型。Python 和 NumPy 都实现了它们自己的(本质上等效的)struct 定义用于复杂变量:

/* Python */
typedef struct {double real; double imag;} Py_complex;

/* NumPy */
typedef struct {float  real, imag;} npy_cfloat;
typedef struct {double real, imag;} npy_cdouble; 

我们本可以实现的是:

%numpy_typemaps(Py_complex , NPY_CDOUBLE, int)
%numpy_typemaps(npy_cfloat , NPY_CFLOAT , int)
%numpy_typemaps(npy_cdouble, NPY_CDOUBLE, int) 

它本可以提供自动转换为 Py_complexnpy_cfloatnpy_cdouble 类型数组的 typemap。然而,不太可能会有任何独立(非 Python,非 NumPy)应用代码,人们会使用SWIG 生成一个 Python 接口,该接口还使用这些复杂类型的定义。更可能的是,这些应用代码将定义自己的复杂类型,或者在 C++ 中使用 std::complex。假设这些数据结构与 Python 和 NumPy 复杂类型兼容,像上面的 %numpy_typemap 展开(将用户的复杂类型替换为第一个参数)应该可以工作。

NumPy 数组标量和 SWIG

SWIG 对于数字类型有复杂的类型检查。例如,如果你的 C/C++ 程序期望一个整数作为输入,SWIG 生成的代码将同时检查 Python 整数和 Python 长整数,并且如果提供的 Python 整数太大而无法缩小成 C 整数时将引发溢出错误。通过在你的 Python 代码中引入 NumPy 标量数组,你可能会从 NumPy 数组中提取一个整数并尝试将其传递给一个期望 intSWIG 封装的 C/C++ 函数,但是SWIG 的类型检查不会将 NumPy 数组标量识别为整数。(通常,这确实可行 - 这取决于 NumPy 是否在使用平台上将你正在使用的整数类型识别为继承自 Python 整数类型。有时候,这意味着在 32 位机器上运行的代码在 64 位机器上会失败。)

如果你遇到一个看起来像下面这样的 Python 错误:

TypeError: in method 'MyClass_MyMethod', argument 2 of type 'int' 

如果你传递的参数是从 NumPy 数组中提取的整数,并且你遇到了这个问题。解决方法是修改SWIG 类型转换系统,以接受 NumPy 数组标量,除了标准整数类型之外。幸运的是,这个功能已经为你提供。只需要复制文件:

pyfragments.swg 

到项目的工作构建目录中,这个问题将会被解决。建议无论如何都这样做,因为这只会增加你的 Python 接口的功能。

为什么会有第二个文件?

SWIG的类型检查和转换系统是 C 宏、SWIG宏、SWIG类型映射和SWIG片段的复杂组合。片段是一种在需要时有条件地将代码插入包装文件中的方法,并且如果不需要则不插入。如果多个类型映射需要相同的片段,则该片段只会在包装代码中插入一次。

有一个用于将 Python 整数转换为 C long的片段。还有一个将 Python 整数转换为 C int的不同片段,并调用long片段中定义的例程。我们可以通过更改long片段的定义来实现所需的更改。SWIG使用“先到先得”系统确定片段的活动定义。也就是说,我们需要在SWIG内部执行之前定义long转换的片段。通过将我们的片段定义放入文件pyfragments.swg中,SWIG允许我们这样做。如果我们将新的片段定义放入numpy.i中,它们将被忽略。

辅助函数

numpy.i文件包含几个宏和例程,它在内部使用这些内容来构建其类型映射。但是,这些功能在接口文件的其他地方也可能会有用。这些宏和例程被实现为片段,在上一节中简要描述。如果您尝试使用以下一个或多个宏或函数,但是您的编译器抱怨找不到该符号,则您需要使用以下方式强制使这些片段出现在代码中:

%fragment("NumPy_Fragments"); 

在你的SWIG接口文件中。

is_array(a)

如果aNULL且可以被转换为PyArrayObject*,则为真。

array_type(a)

a的整数数据类型代码的值,假设a可以被转换为PyArrayObject*

array_numdims(a)

a的维度数目的整数值,假设a可以被转换为PyArrayObject*

array_dimensions(a)

评估为类型为npy_intp且长度为array_numdims(a)的数组,给出a的所有维度的长度,假设a可以被转换为PyArrayObject*

array_size(a,i)

ai维度大小的值,假设a可以被转换为PyArrayObject*

array_strides(a)

评估为类型为npy_intp且长度为array_numdims(a)的数组,给出a的所有维度的步长,假设a可以被转换为PyArrayObject*。步长是元素与沿着相同轴的相邻元素之间的字节距离。

array_stride(a,i)

a的第i个步长的值,假设a可以被转换为PyArrayObject*

array_data(a)

评估为指向a的数据缓冲区的void*类型的指针,假设a可以被转换为PyArrayObject*

array_descr(a)

返回对a的 dtype 属性(PyArray_Descr*)的借用引用,假定a可转换为PyArrayObject*

array_flags(a)

返回表示a的标志的整数,假定a可以转换为PyArrayObject*

array_enableflags(a,f)

设置a的标志(f表示)的标志,假设a可以转换为PyArrayObject*

array_is_contiguous(a)

如果a是一个连续的数组,则评估为 true。等效于(PyArray_ISCONTIGUOUS(a))

array_is_native(a)

如果a的数据缓冲区使用本机字节顺序,则评估为 true。等效于(PyArray_ISNOTSWAPPED(a))

array_is_fortran(a)

如果a是 FORTRAN 排序,则评估为 true。

例程

pytype_string()

返回类型:const char*

参数:

  • PyObject* py_obj,一个一般的 Python 对象。

返回一个描述py_obj类型的字符串。

typecode_string()

返回类型:const char*

参数:

  • int typecode,一个 NumPy 整数类型代码。

返回一个描述与 NumPytypecode对应的类型的字符串。

type_match()

返回类型:int

参数:

  • int actual_type,NumPy 数组的 NumPy 类型代码。
  • int desired_type,期望的 NumPy 类型代码。

确保actual_typedesired_type兼容。例如,这允许字符和字节类型,或整数和长整数类型相匹配。现在等同于PyArray_EquivTypenums()

obj_to_array_no_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个一般的 Python 对象。
  • int typecode,期望的 NumPy 类型代码。

如果合法,将input转换为PyArrayObject*,并确保其为typecode类型。如果无法转换input,或者typecode错误,则设置 Python 错误并返回NULL

obj_to_array_allow_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个一般的 Python 对象。
  • int typecode,结果数组的期望 NumPy 类型代码。
  • int* is_new_object,如果没有执行转换,则返回值为 0,否则为 1。

input转换为具有给定typecode的 NumPy 数组。成功时,返回一个具有正确类型的有效PyArrayObject*。失败时,将设置 Python 错误字符串,并返回NULL

make_contiguous()

返回类型:PyArrayObject*

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • int* is_new_object,如果没有执行转换,则返回值为 0,否则为 1。
  • int min_dims,最小允许的维度。
  • int max_dims,最大允许的维度。

检查ary是否是连续的。如果是,返回输入指针并标记为不是新对象。如果不是连续的,使用原始数据创建一个新的PyArrayObject*,将其标记为新对象并返回指针。

make_fortran()

返回类型:PyArrayObject*

参数

  • PyArrayObject* ary,一个 NumPy 数组。
  • int* is_new_object,如果没有执行转换,则返回值为 0,否则为 1。

检查ary是否是 Fortran 连续的。如果是,则返回输入指针,并将其标记为非新对象。如果它不是 Fortran 连续的,则使用原始数据创建一个新的PyArrayObject*,将其标记为新对象,并返回指针。

obj_to_array_contiguous_allow_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个通用的 Python 对象。
  • int typecode,所得数组的所需 NumPy 类型代码。
  • int* is_new_object,如果没有进行任何转换,则返回 0,否则返回 1。

input转换为所需类型的连续PyArrayObject*。如果输入对象不是连续的PyArrayObject*,则将创建一个新的对象并设置新对象标志。

obj_to_array_fortran_allow_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个通用的 Python 对象。
  • int typecode,所得数组的所需 NumPy 类型代码。
  • int* is_new_object,如果没有进行任何转换,则返回 0,否则返回 1。

input转换为所需类型的 Fortran 连续PyArrayObject*。如果输入对象不是 Fortran 连续的PyArrayObject*,则将创建一个新的对象并设置新对象标志。

require_contiguous()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。

检查ary是否是连续的。如果是,则返回 1。否则,设置 Python 错误并返回 0。

require_native()

返回类型:int

参数:

  • PyArray_Object* ary,一个 NumPy 数组。

要求ary不是字节交换的。如果数组没有字节交换,则返回 1。否则,设置 Python 错误并返回 0。

require_dimensions()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • int exact_dimensions,所需的维度数。

要求ary有指定的维数。如果数组具有指定的维数,则返回 1。否则,设置 Python 错误并返回 0。

require_dimensions_n()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • int* exact_dimensions,表示可接受维度数量的整数数组。
  • int nexact_dimensions的长度。

要求ary具有指定数量的维度之一。如果数组具有指定数量的维度之一,则返回 1。否则,设置 Python 错误字符串并返回 0。

require_size()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • npy_int* size,表示每个维度的所需长度的数组。
  • int nsize的长度。

要求ary具有指定的形状。如果数组具有指定的形状,则返回 1。否则,设置 Python 错误字符串并返回 0。

require_fortran()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。

要求给定的PyArrayObject是 Fortran 有序的。如果PyArrayObject已经是 Fortran 有序的,则不做任何处理。否则,设置 Fortran 排序标志并重新计算步进。

提供的类型映射之外的内容

有许多 C 或 C++数组/NumPy 数组的情况没有通过简单的%include "numpy.i"和后续的%apply指令进行覆盖。

一个常见的例子

考虑一个合理的点积函数原型:

double dot(int len, double* vec1, double* vec2); 

我们想要的 Python 接口是:

def dot(vec1, vec2):
  """
 dot(PyObject,PyObject) -> double
 """ 

这里的问题是只有一个维度参数和两个数组参数,而且我们的类型映射是针对应用于单个数组的维度的(实际上,SWIG不提供一种将len与接受两个 Python 输入参数的vec2关联的机制)。推荐的解决方案如下:

%apply (int DIM1, double* IN_ARRAY1) {(int len1, double* vec1),
                                      (int len2, double* vec2)}
%rename (dot) my_dot;
%exception my_dot {
    $action
    if (PyErr_Occurred()) SWIG_fail;
}
%inline %{
double my_dot(int len1, double* vec1, int len2, double* vec2) {
    if (len1 != len2) {
        PyErr_Format(PyExc_ValueError,
                     "Arrays of lengths (%d,%d) given",
                     len1, len2);
        return 0.0;
    }
    return dot(len1, vec1, vec2);
}
%} 

如果包含double dot()原型的头文件还包含其他你想要包装的原型,以便你需要%include这个头文件,那么你还需要一个%ignore dot;指令,放在%rename之后和%include之前。或者,如果涉及的函数是一个类方法,除了%inline之外,你还需要使用%extend,而不是%ignore

关于错误处理的注解: 注意,my_dot返回一个double但它也可以引发 Python 错误。当向量长度不匹配时,得到的包装器函数将返回 Python 浮点表示为 0.0。由于这不是NULL,Python 解释器将不知道如何检查错误。因此,我们在申请my_dot之前添加%exception指令以获得我们想要的行为(注意$action是一个宏,用于扩展为对my_dot的有效调用)。一般来说,你可能希望编写一个SWIG宏来执行此任务。

其他情况

在遇到其他包装情况时,当你遇到它们时,numpy.i可能会有所帮助。

  • 在某些情况下,你可以使用%numpy_typemaps宏为自己的类型实现类型映射。查看其他常见类型:bool 或其他常见类型:complex 部分以获取示例。另一种情况是,如果你的维度不是int类型(例如,是long类型):

  • %numpy_typemaps(double, NPY_DOUBLE, long) 
    
  • 你可以使用numpy.i中的代码编写自己的类型映射。例如,如果函数参数是一个五维数组,你可以将适当的四维类型映射复制粘贴到你的接口文件中。对于第四维的修改将是微不足道的。

  • 有时,最佳方法是使用%extend指令为你的类定义新方法(或者重载现有方法),这些方法接受一个PyObject*(可以转换为PyArrayObject*)而不是指向缓冲区的指针。在这种情况下,numpy.i中的辅助工具程序可能非常有用。

  • 编写类型映射可能有点不直观。如果您对为 NumPy 编写 SWIG 类型映射有具体问题,numpy.i 的开发人员会监视 Numpy-discussion 和 Swig-user 邮件列表。

最后说明

当您使用 %apply 指令时,通常需要使用 numpy.i,它会一直有效,直到告诉 SWIG 不再需要。如果您要封装的函数或方法的参数具有常见名称,例如 lengthvector,这些类型映射可能会应用于您意料之外或不希望的情况。因此,在完成特定类型映射后,始终最好添加 %clear 指令:

%apply (double* IN_ARRAY1, int DIM1) {(double* vector, int length)}
%include "my_header.h"
%clear (double* vector, int length); 

通常应针对特定地方使用这些类型映射签名,然后在使用完成后清除它们。

总结

默认情况下,numpy.i 提供了支持在 NumPy 数组和 C 数组之间进行转换的类型映射:

  • 可以是 12 种不同的标量类型之一:signed charunsigned charshortunsigned shortintunsigned intlongunsigned longlong longunsigned long longfloatdouble
  • 支持每种数据类型的 74 种不同参数签名,包括:
    • 一维、二维、三维和四维数组。
    • 仅输入、原地、输出参数、输出参数视图和内存管理的输出参数视图行为。
    • 硬编码的维度、数据缓冲区-维度规范和维度-数据缓冲区规范。
    • 支持 C 排序(“最后一个维度最快”)或 Fortran 排序(“第一个维度最快”)的 2D、3D 和 4D 数组。

numpy.i 接口文件还为包装开发者提供了额外的工具,包括:

  • 一个 SWIG 宏 (%numpy_typemaps),具有三个参数,用于为用户选择的 (1) C 数据类型、(2) NumPy 数据类型(假设它们匹配)和 (3) 维度类型 实现 74 个参数签名。
  • 十四个 C 宏和十五个 C 函数,可用于编写专门的类型映射、扩展或处理提供的类型映射未涵盖的情况的内联函数。请注意,这些宏和函数是专门设计用于与 NumPy C/API 一起使用,不管 NumPy 版本号如何,无论 1.6 版本之前和之后的一些 API 弃用后的情况如何。

介绍

简单包装器和接口生成器(或 SWIG)是一个强大的工具,用于为各种脚本语言生成包装器代码。SWIG可以解析头文件,并仅使用代码原型,为目标语言创建接口。但 SWIG 也不是无所不能的。例如,它无法从原型中知道:

double rms(double* seq, int n); 

seq到底是什么。它是一个要就地更改的单个值吗?它是一个数组,如果是的话,它的长度是多少?它只能作为输入吗?输出?输入输出?SWIG 无法确定这些细节,也不会尝试这样做。

如果我们设计了rms,我们可能会将其设计成一个接受长度为ndouble值数组(称为seq)作为输入并返回均方根的常规函数。然而,SWIG的默认行为将是创建一个包装函数,可以编译,但几乎无法按照 C 例程预期的方式从脚本语言中使用。

对于 Python,处理连续(或技术上的跨距)的同质数据块的首选方式是使用 NumPy,它提供了对数据的多维数组的完全面向对象的访问。因此,rms 函数的最合理的 Python 接口应该是(包括文档字符串):

def rms(seq):
  """
 rms: return the root mean square of a sequence
 rms(numpy.ndarray) -> double
 rms(list) -> double
 rms(tuple) -> double
 """ 

seq将是一个由double值组成的 NumPy 数组,其长度n将在传递给 C 例程之前从seq内部提取。更好的是,由于 NumPy 支持从任意 Python 序列构建数组,seq本身可以是一个几乎任意的序列(只要每个元素都可以转换为double),而包装代码将在提取其数据和长度之前在内部将其转换为 NumPy 数组。

SWIG允许通过一种称为typemaps的机制来定义这些类型的转换。本文档提供了如何使用 numpy.i 的信息,这是一个定义了一系列旨在使上面描述的数组相关转换相对简单实现的 SWIG 接口文件。例如,假设上面定义的 rms 函数原型在名为 rms.h 的头文件中。要获得上面讨论的 Python 接口,你的 SWIG 接口文件需要包含以下内容:

%{
#define SWIG_FILE_WITH_INIT
#include "rms.h"
%}

%include "numpy.i"

%init %{
import_array();
%}

%apply (double* IN_ARRAY1, int DIM1) {(double* seq, int n)};
%include "rms.h" 

Typemaps 是根据一个或多个函数参数的列表(按类型或类型和名称)进行匹配的。我们将这样的列表称为签名numpy.i 定义的众多 typemap 之一在上面的代码中使用,其签名为(double* IN_ARRAY1, int DIM1)。参数名称旨在建议 double* 参数是一个一维输入数组,而 int 表示该维度的大小。这恰好是 rms 原型中的模式。

很可能,没有实际的要包装的原型会具有参数名称 IN_ARRAY1DIM1。 我们使用 SWIG %apply 指令将一个维度为 double 的一维输入数组的类型映射应用到 rms 实际使用的原型上。 因此,有效地使用 numpy.i 需要知道可用的类型映射及其作用。

包括上述 SWIG 指令的 SWIG 接口文件将生成类似于以下内容的包装器代码:

 1 PyObject *_wrap_rms(PyObject *args) {
 2   PyObject *resultobj = 0;
 3   double *arg1 = (double *) 0 ;
 4   int arg2 ;
 5   double result;
 6   PyArrayObject *array1 = NULL ;
 7   int is_new_object1 = 0 ;
 8   PyObject * obj0 = 0 ;
 9
10   if (!PyArg_ParseTuple(args,(char *)"O:rms",&obj0)) SWIG_fail;
11   {
12     array1 = obj_to_array_contiguous_allow_conversion(
13                  obj0, NPY_DOUBLE, &is_new_object1);
14     npy_intp size[1] = {
15       -1
16     };
17     if (!array1 || !require_dimensions(array1, 1) ||
18         !require_size(array1, size, 1)) SWIG_fail;
19     arg1 = (double*) array1->data;
20     arg2 = (int) array1->dimensions[0];
21   }
22   result = (double)rms(arg1,arg2);
23   resultobj = SWIG_From_double((double)(result));
24   {
25     if (is_new_object1 && array1) Py_DECREF(array1);
26   }
27   return resultobj;
28 fail:
29   {
30     if (is_new_object1 && array1) Py_DECREF(array1);
31   }
32   return NULL;
33 } 

来自 numpy.i 的类型映射负责以下代码行:12–20,25 和 30。 第 10 行解析输入到 rms 函数。从格式字符串"O:rms",我们可以看到参数列表预期是单个 Python 对象(由冒号前的 O 指定),其指针存储在 obj0 中。 通过调用由 numpy.i 提供的一些函数,可以进行从通用 Python 对象到 NumPy 数组的转换(可能的)进行检查。 这些函数在 辅助函数 部分中有所解释,但希望它们的名称是不言自明的。 第 12 行我们使用 obj0 构造一个 NumPy 数组。 在第 17 行,我们检查结果的有效性:它不为空,并且具有任意长度的单个维度。 一旦验证了这些状态,我们提取数据缓冲区和长度,以便我们可以在第 22 行调用底层 C 函数。 第 25 行对于我们创建了一个不再需要的新数组进行内存管理。

此代码有大量的错误处理。 请注意,SWIG_fail 是一个指向第 28 行标签的 goto fail 的宏。 如果用户提供了错误数量的参数,这将在第 10 行被捕获。 如果构造 NumPy 数组失败或产生了维度错误的数组,这些错误将在第 17 行被捕获。 最后,如果检测到错误,内存在第 30 行仍然被正确地管理。

注意,如果 C 函数签名的顺序不同:

double rms(int n, double* seq); 

SWIG 不会将上述类型映射签名与 rms 的参数列表匹配。 幸运的是,numpy.i 具有一套具有数据指针的类型映射:最后 given last:

%apply (int DIM1, double* IN_ARRAY1) {(int n, double* seq)}; 

这简单地将上面生成的代码中第 3 和第 4 行的 arg1arg2 的定义进行了交换,以及在第 19 和第 20 行的赋值。

使用 numpy.i

numpy.i 文件当前位于 numpy 安装目录下的 tools/swig 子目录中。通常,您会希望将其复制到您正在开发包装器的目录中。

只使用单个 SWIG 接口文件的简单模块应该包括以下内容:

%{
#define SWIG_FILE_WITH_INIT
%}
%include "numpy.i"
%init %{
import_array();
%} 

在编译的 Python 模块中,import_array() 应该只被调用一次。这可以在一个你编写的并与模块链接的 C/C++ 文件中实现。如果是这种情况,则你的接口文件中不应该存在 #define SWIG_FILE_WITH_INIT 或调用 import_array()。或者,这个初始化调用可以在通过SWIG从具有上述 %init 块的接口文件生成的包装文件中实现。如果是这种情况,并且你有多个SWIG接口文件,则只有一个接口文件应该 #define SWIG_FILE_WITH_INIT 并调用 import_array()

可用的类型映射

numpy.i 提供了不同数据类型(如 doubleint)和不同维度类型(如 intlong)的数组的类型映射指令。这些类型映射指令是相同的,只有 C 和 NumPy 类型的规定不同。这些类型映射指令通常通过宏来实现(通常在幕后实现):

%numpy_typemaps(DATA_TYPE, DATA_TYPECODE, DIM_TYPE) 

可以根据适当的 (DATA_TYPE, DATA_TYPECODE, DIM_TYPE) 三元组调用它。例如:

%numpy_typemaps(double, NPY_DOUBLE, int)
%numpy_typemaps(int,    NPY_INT   , int) 

numpy.i 接口文件使用 %numpy_typemaps 宏为以下 C 数据类型和 int 维度类型实现类型映射:

  • signed char
  • unsigned char
  • short
  • unsigned short
  • int
  • unsigned int
  • long
  • unsigned long
  • long long
  • unsigned long long
  • float
  • double

在下面的描述中,我们引用一个通用的 DATA_TYPE,它可以是上述 C 数据类型之一,以及 DIM_TYPE,它应该是许多类型的整数之一。

类型映射的签名在于缓冲区指针的命名方式。FARRAY 命名方式适用于 Fortran 排序的数组,ARRAY 命名方式适用于 C 排序(或 1D 数组)。

输入数组

输入数组被定义为传递给例程但不进行原地修改或返回给用户的数据的数组。因此,Python 输入数组可以是几乎任何可以转换为请求类型数组的 Python 序列(如列表)。输入数组的签名为

1D:

  • ( DATA_TYPE IN_ARRAY1[ANY] )
  • ( DATA_TYPE* IN_ARRAY1, int DIM1 )
  • ( int DIM1, DATA_TYPE* IN_ARRAY1 )

2D:

  • ( DATA_TYPE IN_ARRAY2[ANY][ANY] )
  • ( DATA_TYPE* IN_ARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, DATA_TYPE* IN_ARRAY2 )
  • ( DATA_TYPE* IN_FARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, DATA_TYPE* IN_FARRAY2 )

3D:

  • ( DATA_TYPE IN_ARRAY3[ANY][ANY][ANY] )
  • ( DATA_TYPE* IN_ARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, DATA_TYPE* IN_ARRAY3 )
  • ( DATA_TYPE* IN_FARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, DATA_TYPE* IN_FARRAY3 )

4D:

  • (DATA_TYPE IN_ARRAY4[ANY][ANY][ANY][ANY])
  • (DATA_TYPE* IN_ARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, , DIM_TYPE DIM4, DATA_TYPE* IN_ARRAY4)
  • (DATA_TYPE* IN_FARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4, DATA_TYPE* IN_FARRAY4)

列出的第一个签名,( DATA_TYPE IN_ARRAY[ANY] )是用于具有硬编码维度的一维数组。同样,( DATA_TYPE IN_ARRAY2[ANY][ANY] )用于具有硬编码维度的二维数组,三维数组类似。

原位数组

原位数组定义为在原地修改的数组。输入值可能会被使用,也可能不会被使用,但在函数返回时的值是重要的。因此,提供的 Python 参数必须是所需类型的 NumPy 数组。原地签名为

1D:

  • ( DATA_TYPE INPLACE_ARRAY1[ANY] )
  • ( DATA_TYPE* INPLACE_ARRAY1, int DIM1 )
  • ( int DIM1, DATA_TYPE* INPLACE_ARRAY1 )

2D:

  • ( DATA_TYPE INPLACE_ARRAY2[ANY][ANY] )
  • ( DATA_TYPE* INPLACE_ARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, DATA_TYPE* INPLACE_ARRAY2 )
  • ( DATA_TYPE* INPLACE_FARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, DATA_TYPE* INPLACE_FARRAY2 )

3D:

  • ( DATA_TYPE INPLACE_ARRAY3[ANY][ANY][ANY] )
  • ( DATA_TYPE* INPLACE_ARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, DATA_TYPE* INPLACE_ARRAY3 )
  • ( DATA_TYPE* INPLACE_FARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, DATA_TYPE* INPLACE_FARRAY3 )

4D:

  • (DATA_TYPE INPLACE_ARRAY4[ANY][ANY][ANY][ANY])
  • (DATA_TYPE* INPLACE_ARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, , DIM_TYPE DIM4, DATA_TYPE* INPLACE_ARRAY4)
  • (DATA_TYPE* INPLACE_FARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4, DATA_TYPE* INPLACE_FARRAY4)

这些 typemap 现在检查以确保INPLACE_ARRAY参数使用本机字节顺序。如果不是,则会引发异常。

还有一种“平坦”的原地数组,用于您希望修改或处理每个元素的情况,无论维度的数量如何。一个例子是一个在原地量化数组的“量化”函数,无论是 1D、2D 还是其他维度,都可以对每个元素进行量化。这种形式检查连续性,但允许 C 或 Fortran 排序。

ND:

  • (DATA_TYPE* INPLACE_ARRAY_FLAT, DIM_TYPE DIM_FLAT)

Argout 数组

Argout 数组是在 C 中作为输入参数出现的数组,但实际上是输出数组。当存在多个输出变量且单个返回参数不足时,此模式经常出现。在 Python 中,返回多个参数的常规方法是将它们打包到一个序列(元组、列表等)中并返回该序列。这就是 argout 类型映射所做的。如果包装的函数使用了这些 argout 类型映射并且具有多个返回参数,它们将被打包成一个元组或列表,具体取决于 Python 的版本。Python 用户不会传入这些数组,它们只是被返回。对于指定了维度的情况,Python 用户必须将该维度作为参数提供。argout 签名为

1D:

  • ( 数据类型 ARGOUT_ARRAY1[任意] )
  • ( DATA_TYPE* ARGOUT_ARRAY1, int DIM1 )
  • ( int DIM1, DATA_TYPE* ARGOUT_ARRAY1 )

2D:

  • ( DATA_TYPE ARGOUT_ARRAY2[任意][任意] )

3D:

  • ( DATA_TYPE ARGOUT_ARRAY3[任意][任意][任意] )

4D:

  • ( DATA_TYPE ARGOUT_ARRAY4[任意][任意][任意][任意] )

这些通常用于在 C/C++ 中分配一个或多个数组在堆上,并调用函数填充数组的值的情况。在 Python 中,这些数组会为您分配并作为新的数组对象返回。

请注意,我们支持 1D 中的 DATA_TYPE* argout 类型映射,但不支持 2D 或 3D。这是因为 SWIG 类型映射语法的一个怪癖,无法避免。对于这些类型的 1D 类型映射,Python 函数将接受一个表示 DIM1 的单个参数。

Argout 视图数组

当您的 C 代码提供了对其内部数据的视图并且不需要用户分配任何内存时,argoutview 数组就派上了用场。这可能是危险的。几乎没有办法保证 C 代码的内部数据会在封装它的 NumPy 数组的整个生命周期内保持存在。如果用户在销毁提供数据视图的对象之前销毁了封装它的 NumPy 数组,那么使用该数组可能导致坏的内存引用或分段错误。尽管如此,在处理大型数据集的情况下,有时您简直别无选择。

要包装成 argoutview 数组的 C 代码特征为指针:指向维度和指向数据的双指针,以便将这些值传回给用户。因此,argoutview 类型映射签名为

1D:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY1, DIM_TYPE* DIM1 )
  • ( DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEW_ARRAY1 )

2D:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2 )
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEW_ARRAY2 )
  • ( DATA_TYPE** ARGOUTVIEW_FARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2 )
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEW_FARRAY2 )

3D:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEW_ARRAY3)
  • (DATA_TYPE** ARGOUTVIEW_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEW_FARRAY3)

4D:

  • (DATA_TYPE** ARGOUTVIEW_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_ARRAY4)
  • (DATA_TYPE** ARGOUTVIEW_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_FARRAY4)

注意,不支持具有硬编码维度的数组。这些不能遵循这些类型映射的双指针签名。

内存管理的输出视图数组

numpy.i 的一个最新添加的功能是允许具有管理内存的输出数组的类型映射。请参见 此处的讨论

1D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY1, DIM_TYPE* DIM1)
  • (DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEWM_ARRAY1)

2D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_ARRAY2)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_FARRAY2)

3D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_ARRAY3)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_FARRAY3)

4D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_ARRAY4)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_FARRAY4)

输出数组

numpy.i 接口文件不支持输出数组的类型映射,原因有几点。首先,C/C++ 返回参数限制为单个值。这阻止了以一般方式获取维度信息。其次,不允许返回参数是具有硬编码长度的数组。换句话说:

double[3] newVector(double x, double y, double z); 

不是合法的 C/C++ 语法。因此,我们无法提供以下形式的类型映射:

%typemap(out) (TYPE[ANY]); 

如果你遇到一个函数或方法返回指向数组的指针的情况,你最好写一个自己的函数版本来进行包装,对于类方法可以使用 %extend,对于函数可以使用 %ignore%rename

其他常见类型:bool

注意,在 可用类型映射 部分,C++ 类型 bool 不受支持。NumPy 的布尔值占用一个字节,而 C++ 的 bool 占用四个字节(至少在我的系统上是这样)。因此:

%numpy_typemaps(bool, NPY_BOOL, int) 

将导致 Typemaps 产生引用不正确数据长度的代码。您可以实现以下宏扩展:

%numpy_typemaps(bool, NPY_UINT, int) 

来解决数据长度问题,输入数组 可能有效,但原位数组 可能无法通过类型检查。

其他常见类型:复数

复杂浮点类型的 Typemap 转换也不能自动支持。这是因为 Python 和 NumPy 是用 C 编写的,它们没有本地复杂类型。Python 和 NumPy 都实现了它们自己的(本质上等效的)struct定义,用于复杂变量:

/* Python */
typedef struct {double real; double imag;} Py_complex;

/* NumPy */
typedef struct {float  real, imag;} npy_cfloat;
typedef struct {double real, imag;} npy_cdouble; 

我们本来可以实现:

%numpy_typemaps(Py_complex , NPY_CDOUBLE, int)
%numpy_typemaps(npy_cfloat , NPY_CFLOAT , int)
%numpy_typemaps(npy_cdouble, NPY_CDOUBLE, int) 

这将为Py_complexnpy_cfloatnpy_cdouble类型的数组提供自动类型转换。然而,似乎不太可能有任何独立的(非 Python、非 NumPy)应用代码会使用 SWIG 生成 Python 接口,并且还使用这些定义的复杂类型。更有可能的是,这些应用代码将定义它们自己的复杂类型,或者在 C++的情况下使用std::complex。假设这些数据结构与 Python 和 NumPy 的复杂类型兼容,那么以上的%numpy_typemap扩展(用用户的复杂类型替换第一个参数)应该有效。

输入数组

输入数组被定义为传入程序但不会被直接更改或返回给用户的数据数组。因此,Python 输入数组几乎可以是任何可转换为所请求数组类型的 Python 序列(如列表)。输入数组签名为

1D:

  • ( DATA_TYPE IN_ARRAY1[ANY] )
  • ( DATA_TYPE* IN_ARRAY1, int DIM1 )
  • ( int DIM1, DATA_TYPE* IN_ARRAY1 )

2D:

  • ( DATA_TYPE IN_ARRAY2[ANY][ANY] )
  • ( DATA_TYPE* IN_ARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, DATA_TYPE* IN_ARRAY2 )
  • ( DATA_TYPE* IN_FARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, DATA_TYPE* IN_FARRAY2 )

3D:

  • ( DATA_TYPE IN_ARRAY3[ANY][ANY][ANY] )
  • ( DATA_TYPE* IN_ARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, DATA_TYPE* IN_ARRAY3 )
  • ( DATA_TYPE* IN_FARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, DATA_TYPE* IN_FARRAY3 )

4D:

  • (DATA_TYPE IN_ARRAY4[ANY][ANY][ANY][ANY])
  • (DATA_TYPE* IN_ARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, , DIM_TYPE DIM4, DATA_TYPE* IN_ARRAY4)
  • (DATA_TYPE* IN_FARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4, DATA_TYPE* IN_FARRAY4)

列出的第一个签名,( DATA_TYPE IN_ARRAY[ANY] ),是给定固定维度的一维数组。同样,( DATA_TYPE IN_ARRAY2[ANY][ANY] )是给定固定维度的二维数组,类似地适用于三维数组。

原位数组

In-place arrays are defined as arrays that are modified in-place. The input values may or may not be used, but the values at the time the function returns are significant. The provided Python argument must therefore be a NumPy array of the required type. The in-place signatures are

1D:

  • ( DATA_TYPE INPLACE_ARRAY1[ANY] )
  • ( DATA_TYPE* INPLACE_ARRAY1, int DIM1 )
  • ( int DIM1, DATA_TYPE* INPLACE_ARRAY1 )

2D:

  • ( DATA_TYPE INPLACE_ARRAY2[ANY][ANY] )
  • ( DATA_TYPE* INPLACE_ARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, DATA_TYPE* INPLACE_ARRAY2 )
  • ( DATA_TYPE* INPLACE_FARRAY2, int DIM1, int DIM2 )
  • ( int DIM1, int DIM2, DATA_TYPE* INPLACE_FARRAY2 )

3D:

  • ( DATA_TYPE INPLACE_ARRAY3[ANY][ANY][ANY] )
  • ( DATA_TYPE* INPLACE_ARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, DATA_TYPE* INPLACE_ARRAY3 )
  • ( DATA_TYPE* INPLACE_FARRAY3, int DIM1, int DIM2, int DIM3 )
  • ( int DIM1, int DIM2, int DIM3, DATA_TYPE* INPLACE_FARRAY3 )

4D:

  • (DATA_TYPE INPLACE_ARRAY4[ANY][ANY][ANY][ANY])
  • (DATA_TYPE* INPLACE_ARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, , DIM_TYPE DIM4, DATA_TYPE* INPLACE_ARRAY4)
  • (DATA_TYPE* INPLACE_FARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4, DATA_TYPE* INPLACE_FARRAY4)

These typemaps now check to make sure that the INPLACE_ARRAY arguments use native byte ordering. If not, an exception is raised.

There is also a “flat” in-place array for situations in which you would like to modify or process each element, regardless of the number of dimensions. One example is a “quantization” function that quantizes each element of an array in-place, be it 1D, 2D or whatever. This form checks for continuity but allows either C or Fortran ordering.

ND:

  • (DATA_TYPE* INPLACE_ARRAY_FLAT, DIM_TYPE DIM_FLAT)

Argout Arrays

Argout arrays are arrays that appear in the input arguments in C, but are in fact output arrays. This pattern occurs often when there is more than one output variable and the single return argument is therefore not sufficient. In Python, the conventional way to return multiple arguments is to pack them into a sequence (tuple, list, etc.) and return the sequence. This is what the argout typemaps do. If a wrapped function that uses these argout typemaps has more than one return argument, they are packed into a tuple or list, depending on the version of Python. The Python user does not pass these arrays in, they simply get returned. For the case where a dimension is specified, the python user must provide that dimension as an argument. The argout signatures are

1D:

  • ( DATA_TYPE ARGOUT_ARRAY1[ANY] )
  • ( DATA_TYPE* ARGOUT_ARRAY1, int DIM1 )
  • ( int DIM1, DATA_TYPE* ARGOUT_ARRAY1 )

2D:

  • ( DATA_TYPE ARGOUT_ARRAY2[ANY][ANY] )

3D:

  • ( DATA_TYPE ARGOUT_ARRAY3[ANY][ANY][ANY] )

4D:

  • ( DATA_TYPE ARGOUT_ARRAY4[ANY][ANY][ANY][ANY] )

这些通常用于在 C/C++中,您会在堆上分配一个或多个数组,并调用函数填充数组的值的情况。 在 Python 中,数组将为您分配并返回为新的数组对象。

请注意,我们支持DATA_TYPE* argout typemaps 在 1D 中,但不支持 2D 或 3D。 这是由于SWIG typemap 语法的一个怪癖,无法避免。 请注意,对于这些类型的 1D typemaps,Python 函数将采用一个表示DIM1的单个参数。

输出视图数组

Argoutview 数组用于当您的 C 代码向您提供其内部数据的视图且不需要用户分配任何内存时。 这可能很危险。 几乎没有办法保证来自 C 代码的内部数据将在封装它的 NumPy 数组的整个生命周期中保持存在。 如果用户在销毁提供数据视图的对象之前销毁了封装该数组的 NumPy 数组,那么使用该数组可能会导致不良的内存引用或段错误。 尽管如此,在处理大型数据集的情况下,有时只有这一个选择。

要为 argoutview 数组包装的 C 代码以指针为特征:指向尺寸和指向数据的双指针,以便将这些值传递回用户。 因此,argoutview typemap 签名为

1D:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY1, DIM_TYPE* DIM1 )
  • ( DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEW_ARRAY1 )

2D:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2 )
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEW_ARRAY2 )
  • (DATA_TYPE** ARGOUTVIEW_FARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEW_FARRAY2 )

3D:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEW_ARRAY3)
  • ( DATA_TYPE** ARGOUTVIEW_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEW_FARRAY3)

4D:

  • (DATA_TYPE** ARGOUTVIEW_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_ARRAY4)
  • (DATA_TYPE** ARGOUTVIEW_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_FARRAY4)

请注意,不支持具有硬编码尺寸的数组。 这些无法遵循这些 typemaps 的双指针签名。

内存管理的 Argout 视图数组

numpy.i最近新增了允许管理视图的 argout 数组的 typemaps。请查看这里的讨论。

1D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY1, DIM_TYPE* DIM1)
  • (DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEWM_ARRAY1)

2D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_ARRAY2)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_FARRAY2)

3D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_ARRAY3)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_FARRAY3)

4D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_ARRAY4)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_FARRAY4)

输出数组

numpy.i 接口文件不支持用于输出数组的 typemaps,原因有几点。首先,C/C++ 返回参数限制为单个值。这阻止了以一般方式获取维度信息。其次,不允许作为返回参数的硬编码长度的数组。换句话说:

double[3] newVector(double x, double y, double z); 

不是合法的 C/C++ 语法。因此,我们不能提供以下形式的 typemaps:

%typemap(out) (TYPE[ANY]); 

如果遇到函数或方法返回指向数组的指针的情况,最好编写要包装的函数的自定义版本,对于类方法的情况,可以使用%extend,对于函数的情况,可以使用%ignore%rename

其他常见类型:布尔值

注意:C++ 类型bool在可用类型映射部分不受支持。 NumPy 的布尔值占用一个字节,而 C++ 的bool占用四个字节(至少在我的系统上是这样)。因此:

%numpy_typemaps(bool, NPY_BOOL, int) 

会产生引用错误数据长度的 typemaps。您可以实现以下宏扩展:

%numpy_typemaps(bool, NPY_UINT, int) 

为了解决数据长度问题,输入数组将正常工作,但原位数组可能会失败类型检查。

其他常见类型:复数

对于复杂浮点类型的 typemap 转换也不会自动支持。这是因为 Python 和 NumPy 是用 C 编写的,没有本地复杂类型。 Python 和 NumPy 都实现了自己的(本质上等效的)struct定义以用于复数变量:

/* Python */
typedef struct {double real; double imag;} Py_complex;

/* NumPy */
typedef struct {float  real, imag;} npy_cfloat;
typedef struct {double real, imag;} npy_cdouble; 

我们本来可以实现:

%numpy_typemaps(Py_complex , NPY_CDOUBLE, int)
%numpy_typemaps(npy_cfloat , NPY_CFLOAT , int)
%numpy_typemaps(npy_cdouble, NPY_CDOUBLE, int) 

这将为Py_complexnpy_cfloatnpy_cdouble类型的数组提供自动类型转换。但是,这些定义对于复数类型的独立(非 Python,非 NumPy)应用代码的使用似乎不太可能使用 SWIG 生成 Python 接口,并且使用这些定义处理复数类型。更可能的是,这些应用代码将定义它们自己的复数类型,或者在 C++ 的情况下,使用std::complex。假设这些数据结构与 Python 和 NumPy 的复数类型兼容,那么以上(用用户的复数类型代替第一个参数)的%numpy_typemap扩展应该起作用。

NumPy 数组标量和 SWIG

SWIG 对数值类型有复杂的类型检查。例如,如果你的 C/C++ 程序期望整数作为输入,那么由 SWIG 生成的代码将同时检查 Python 整数和 Python 长整数,并且如果提供的 Python 整数太大而无法转换为 C 整数,则会引发溢出错误。引入 NumPy 标量数组到你的 Python 代码中,你可能会从 NumPy 数组中提取整数,并尝试将其传递给一个期望intSWIG 包装的 C/C++ 函数,但是 SWIG 的类型检查不会将 NumPy 数组标量识别为整数。(通常情况下,这实际上是有效的-这取决于 NumPy 是否识别你所用的整数类型作为继承于你所用平台上的 Python 整数类型。有时,这意味着在 32 位机器上工作的代码在 64 位机器上会失败。)

如果你遇到一个看起来像下面的 Python 错误:

TypeError: in method 'MyClass_MyMethod', argument 2 of type 'int' 

并且你传递的参数是从 NumPy 数组中提取的整数,那么你就遇到了这个问题。解决方法是修改 SWIG 的类型转换系统以接受 NumPy 数组标量,除了标准整数类型之外。幸运的是,这个功能已经为你提供了。只需复制文件:

pyfragments.swg 

到你的项目工作构建目录,并且这个问题就会被解决。建议你无论如何都这样做,因为它只会增加你的 Python 接口的功能。

为什么还有第二个文件?

SWIG的类型检查和转换系统是复杂的 C 宏、SWIG 宏、SWIG 类型映射和 SWIG 片段的复杂组合。片段是一种在需要时有条件地将代码插入到包装文件中的方法,如果不需要,则不插入。如果多个类型映射需要同一个片段,那么片段只会被插入你的包装代码一次。

有一个片段用于将 Python 整数转换为 C 的long。有另一个片段将 Python 整数转换为 C 的int,并调用在long片段中定义的例程。我们可以通过更改long片段的定义在这里做我们想要的更改。SWIG使用“先到先服务”系统来确定片段的活动定义。也就是说,我们需要在SWIG内部执行之前为long转换定义片段。SWIG允许我们通过将我们的片段定义放在文件pyfragments.swg中来实现这一点。如果我们将新的片段定义放在numpy.i中,它们将被忽略。

为什么有第二个文件?

SWIG类型检查和转换系统是 C 宏、SWIG宏、SWIG类型映射和SWIG片段的复杂组合。片段是一种在需要时有条件地将代码插入到您的包装文件中的方法,在不需要时不插入。如果多个类型映射需要相同的片段,则该片段只会被插入到您的包装代码中一次。

有一个片段用于将 Python 整数转换为 C 的long。还有一个不同的片段将 Python 整数转换为 C 的int,它调用long片段中定义的例程。我们可以通过更改long片段的定义在这里做我们想要的更改。SWIG使用“先到先服务”系统来确定片段的活动定义。也就是说,我们需要在SWIG内部执行之前为long转换定义片段。SWIG允许我们通过将我们的片段定义放在文件pyfragments.swg中来实现这一点。如果我们将新的片段定义放在numpy.i中,它们将被忽略。

辅助函数

文件numpy.i包含许多宏和例程,它们在内部用于构建其类型映射。但是,这些函数可能在接口文件中其他地方也有用。这些宏和例程被实现为片段,在前一节中简要描述了这些片段。如果您尝试使用以下一个或多个宏或函数,但是您的编译器抱怨它不能识别该符号,那么您需要使用以下方法强制这些片段出现在您的代码中:

%fragment("NumPy_Fragments"); 

在您的SWIG接口文件中。

is_array(a)

如果a是非NULL并且可以转换为PyArrayObject*,则求值为真。

array_type(a)

a 的整数数据类型代码求值,假设a可以转换为PyArrayObject*

array_numdims(a)

a的维数求值,假设a可以转换为PyArrayObject*

array_dimensions(a)

评估为类型为npy_intp且长度为array_numdims(a)的数组,给出a的所有维度的长度,假设a可以转换为PyArrayObject*

array_size(a,i)

评估为a的第i个维度大小,假设a可以转换为PyArrayObject*

array_strides(a)

评估为类型为npy_intp且长度为array_numdims(a)的数组,给出a的所有维度的跨度,假设a可以转换为PyArrayObject*。跨度是元素与沿同一轴的相邻元素之间的字节距离。

array_stride(a,i)

评估为a的第i个跨度,假设a可以转换为PyArrayObject*

array_data(a)

评估为指向a的数据缓冲区的void*类型的指针,假设a可以转换为PyArrayObject*

array_descr(a)

返回对a的 dtype 属性(PyArray_Descr*)的借用引用,假设a可以转换为PyArrayObject*

array_flags(a)

返回一个整数,表示a的标志,假设a可以转换为PyArrayObject*

array_enableflags(a,f)

设置a的标志f,假设a可以转换为PyArrayObject*

array_is_contiguous(a)

如果a是一个连续数组,则评估为真。等同于(PyArray_ISCONTIGUOUS(a))

array_is_native(a)

如果a的数据缓冲区使用本机字节顺序,则为真。等同于(PyArray_ISNOTSWAPPED(a))

array_is_fortran(a)

如果a按 FORTRAN 顺序排列,则评估为真。

程序

pytype_string()

返回类型:const char*

参数:

  • PyObject* py_obj,一个通用的 Python 对象。

返回描述py_obj类型的字符串。

typecode_string()

返回类型:const char*

参数:

  • int typecode,一个 NumPy 整数类型码。

返回描述与 NumPy typecode对应的类型的字符串。

type_match()

返回类型:int

参数:

  • int actual_type,NumPy 数组的 NumPy 类型码。
  • int desired_type,所需的 NumPy 类型码。

确保actual_typedesired_type兼容。例如,这允许字符和字节类型,或者 int 和 long 类型匹配。现在等同于PyArray_EquivTypenums()

obj_to_array_no_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个通用的 Python 对象。
  • int typecode,所需的 NumPy 类型码。

如果合法,将input转换为PyArrayObject*,并确保其类型为typecode。如果无法转换input,或者typecode错误,则设置 Python 错误并返回NULL

obj_to_array_allow_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个通用的 Python 对象。
  • int typecode,结果数组的所需的 NumPy 类型码。
  • int* is_new_object,如果未执行任何转换,则返回值为 0,否则为 1。

input转换为具��给定typecode的 NumPy 数组。成功时,返回具有正确类型的有效PyArrayObject*。失败时,将设置 Python 错误字符串,并且该例程返回NULL

make_contiguous()

返回类型: PyArrayObject*

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • int* is_new_object,如果没有进行转换则返回值为 0,否则返回 1。
  • int min_dims,最小可允许的维度。
  • int max_dims,最大允许的维度。

检查ary是否是连续的。如果是,则返回输入指针并标记为不是新对象。如果不是连续的,则使用原始数据创建一个新的PyArrayObject*,标记为新对象并返回指针。

make_fortran()

返回类型: PyArrayObject*

参数

  • PyArrayObject* ary,一个 NumPy 数组。
  • int* is_new_object,如果没有进行转换则返回值为 0,否则返回 1。

检查ary是否是 Fortran 连续的。如果是,则返回输入指针并标记为不是新对象。如果不是 Fortran 连续的,则使用原始数据创建一个新的PyArrayObject*,标记为新对象并返回指针。

obj_to_array_contiguous_allow_conversion()

返回类型: PyArrayObject*

参数:

  • PyObject* input,一个一般的 Python 对象。
  • int typecode,所得数组的期望 NumPy 类型代码。
  • int* is_new_object,如果没有进行转换则返回值为 0,否则返回 1。

input转换为连续的PyArrayObject*以指定类型。如果输入对象不是连续的PyArrayObject*,将创建一个新对象并设置新对象标志。

obj_to_array_fortran_allow_conversion()

返回类型: PyArrayObject*

参数:

  • PyObject* input,一个一般的 Python 对象。
  • int typecode,所得数组的期望 NumPy 类型代码。
  • int* is_new_object,如果没有进行转换则返回值为 0,否则返回 1。

input转换为指定类型的 Fortran 连续PyArrayObject*。如果输入对象不是 Fortran 连续的PyArrayObject*,将创建一个新对象并设置新对象标志。

require_contiguous()

返回类型: int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。

测试ary是否是连续的。如果是,则返回 1。否则,设置 Python 错误并返回 0。

require_native()

返回类型: int

参数:

  • PyArray_Object* ary,一个 NumPy 数组。

要求ary不进行字节交换。如果数组没有进行字节交换,则返回 1。否则,设置 Python 错误并返回 0。

require_dimensions()

返回类型: int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • int exact_dimensions,期望的维度数。

要求ary具有指定数量的维度。如果数组具有指定数量的维度,则返回 1。否则,设置 Python 错误并返回 0。

require_dimensions_n()

返回类型: int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • int* exact_dimensions,表示可接受维度数量的整数数组。
  • int nexact_dimensions的长度。

要求ary具有指定数量的维度之一。如果数组具有指定数量的维度之一,则返回 1。否则,设置 Python 错误字符串并返回 0。

require_size()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • npy_int* size,表示每个维度的期望长度的数组。
  • int nsize的长度。

要求ary具有指定的形状。如果数组具有指定的形状,则返回 1。否则,设置 Python 错误字符串并返回 0。

require_fortran()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。

要求给定的PyArrayObject是 Fortran 排序的。如果PyArrayObject已经是 Fortran 排序的,则不执行任何操作。否则,设置 Fortran 排序标志并重新计算步幅。

is_array(a)

如果a是非NULL并且可以转换为PyArrayObject*,则评估为真。

array_type(a)

假设可以将a转换为PyArrayObject*,评估a的整数数据类型代码。

array_numdims(a)

假设可以将a转换为PyArrayObject*,评估a的维度的整数值。

array_dimensions(a)

假设可以将a转换为PyArrayObject*,评估一个类型为npy_intp且长度为array_numdims(a)的数组,给出a的所有维度的长度。

array_size(a,i)

假设可以将a转换为PyArrayObject*,评估a的第i维度大小。

array_strides(a)

假设可以将a转换为PyArrayObject*,评估一个类型为npy_intp且长度为array_numdims(a)的数组,给出a的所有维度的步幅。步幅是一个元素与其在同一轴上的相邻元素之间的字节距离。

array_stride(a,i)

假设可以将a转换为PyArrayObject*,评估a的第i个步幅。

array_data(a)

假设可以将a转换为PyArrayObject*,评估一个指向a的数据缓冲区的void*类型指针。

array_descr(a)

返回一个对a的 dtype 属性(PyArray_Descr*)的借用引用,假设可以将a转换为PyArrayObject*

array_flags(a)

返回表示a的标志的整数,假设可以将a转换为PyArrayObject*

array_enableflags(a,f)

设置代表af标志,假设可以将a转换为PyArrayObject*

array_is_contiguous(a)

如果a是一个连续的数组,则评估为真。相当于(PyArray_ISCONTIGUOUS(a))

array_is_native(a)

如果a的数据缓冲区使用本机字节顺序,则评估为真。相当于(PyArray_ISNOTSWAPPED(a))

array_is_fortran(a)

如果a按照 FORTRAN 排序,则评估为真。

例程

pytype_string()

返回类型:const char*

参数:

  • PyObject* py_obj,一个通用的 Python 对象。

返回描述py_obj类型的字符串。

typecode_string()

返回类型:const char*

参数:

  • int typecode,一个 NumPy 整数类型码。

返回一个描述与 NumPy typecode 对应的类型的字符串。

type_match()

返回类型:int

参数:

  • int actual_type,一个 NumPy 数组的 NumPy 类型码。
  • int desired_type,所需的 NumPy 类型码。

确保 actual_typedesired_type 兼容。例如,这允许字符和字节类型,或整数和长整数类型匹配。这现在等同于 PyArray_EquivTypenums()

obj_to_array_no_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个通用的 Python 对象。
  • int typecode,所需的 NumPy 类型码。

如果合法,将 input 强制转换为 PyArrayObject*,并确保其为 typecode 类型。如果无法强制转换 input,或者 typecode 错误,则设置 Python 错误并返回 NULL

obj_to_array_allow_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个通用的 Python 对象。
  • int typecode,所需的 NumPy 类型码。
  • int* is_new_object,如果没有进行转换,则返回 0,否则返回 1。

input 转换成具有给定 typecode 的 NumPy 数组。成功时,返回具有正确类型的有效 PyArrayObject*。失败时,将设置 Python 错误字符串,并返回 NULL

make_contiguous()

返回类型:PyArrayObject*

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • int* is_new_object,如果没有进行转换,则返回 0,否则返回 1。
  • int min_dims,最小可允许的维数。
  • int max_dims,最大可允许的维数。

检查 ary 是否是连续的。如果是,则返回输入指针,并标记为非新对象。如果它不是连续的,使用原始数据创建一个新的 PyArrayObject*,并将其标记为新对象,然后返回指针。

make_fortran()

返回类型:PyArrayObject*

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • int* is_new_object,如果没有进行转换,则返回 0,否则返回 1。

检查 ary 是否是 Fortran 连续的。如果是,则返回输入指针,并标记为非新对象。如果它不是 Fortran 连续的,使用原始数据创建一个新的 PyArrayObject*,并将其标记为新对象,然后返回指针。

obj_to_array_contiguous_allow_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个通用的 Python 对象。
  • int typecode,所需的 NumPy 类型码。
  • int* is_new_object,如果没有进行转换,则返回 0,否则返回 1。

input 转换成指定类型的连续的 PyArrayObject*。如果输入对象不是连续的 PyArrayObject*,则会创建一个新对象,并设置新对象标志。

obj_to_array_fortran_allow_conversion()

返回类型:PyArrayObject*

参数:

  • PyObject* input,一个通用的 Python 对象。
  • int typecode,所需的 NumPy 类型码。
  • int* is_new_object,如果没有进行转换,则返回 0,否则返回 1。

input转换为指定类型的 Fortran 连续的PyArrayObject*。如果输入对象不是 Fortran 连续的PyArrayObject*,将创建一个新对象并设置新对象标志。

require_contiguous()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。

测试ary是否是连续的。如果是,则返回 1。否则,设置 Python 错误并返回 0。

require_native()

返回类型:int

参数:

  • PyArray_Object* ary,一个 NumPy 数组。

要求ary不是字节交换的。如果数组不是字节交换的,返回 1。否则,设置 Python 错误并返回 0。

require_dimensions()

返回类型:int

参数:

  • PyArrayObject* ary���一个 NumPy 数组。
  • int exact_dimensions,所需的维数。

要求ary具有指定数量的维数。如果数组具有指定数量的维数,则返回 1。否则,设置 Python 错误并返回 0。

require_dimensions_n()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • int* exact_dimensions,表示可接受维数的整数数组。
  • int nexact_dimensions的长度。

要求ary具有指定维数列表中的一个。如果数组具有指定数量的维数之一,则返回 1。否则,设置 Python 错误字符串并返回 0。

require_size()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。
  • npy_int* size,表示每个维度的期望长度的数组。
  • int nsize的长度。

要求ary具有指定形状。如果数组具有指定形状,则返回 1。否则,设置 Python 错误字符串并返回 0。

require_fortran()

返回类型:int

参数:

  • PyArrayObject* ary,一个 NumPy 数组。

要求给定的PyArrayObject是 Fortran 排序的。如果PyArrayObject已经是 Fortran 排序的,则不执行任何操作。否则,设置 Fortran 排序标志并重新计算步幅。

超出所提供的类型映射

有许多 C 或 C++数组/NumPy 数组的情况不符合简单的%include "numpy.i"和随后的%apply指令。

一个常见的例子

考虑一个合理的点积函数原型:

double dot(int len, double* vec1, double* vec2); 

我们想要的 Python 接口是:

def dot(vec1, vec2):
  """
 dot(PyObject,PyObject) -> double
 """ 

这里的问题是有一个维度参数和两个数组参数,并且我们的类型映射是为一个数组应用于单个数组而设置的(事实上,SWIG没有提供将lenvec2关联起来需要两个 Python 输入参数的机制)。推荐的解决方案如下:

%apply (int DIM1, double* IN_ARRAY1) {(int len1, double* vec1),
                                      (int len2, double* vec2)}
%rename (dot) my_dot;
%exception my_dot {
    $action
    if (PyErr_Occurred()) SWIG_fail;
}
%inline %{
double my_dot(int len1, double* vec1, int len2, double* vec2) {
    if (len1 != len2) {
        PyErr_Format(PyExc_ValueError,
                     "Arrays of lengths (%d,%d) given",
                     len1, len2);
        return 0.0;
    }
    return dot(len1, vec1, vec2);
}
%} 

如果包含double dot()原型的头文件还包含其他您想要包装的原型,因此需要%include这个头文件,那么您还需要一个%ignore dot;指令,在%rename后面和%include前面放置。或者,如果问题函数是类方法,则您将需要在%inline之外使用%extend而不是使用%ignore指令。

关于错误处理的注意事项:请注意,my_dot 返回一个 double 值,但它也可能引发 Python 错误。当向量长度不匹配时,生成的包装函数将返回 Python 中的浮点表示 0.0。因为这不是 NULL,所以 Python 解释器不会检查错误。因此,我们在上面为 my_dot 添加了 %exception 指令以获得所需的行为(请注意,$action 是一个宏,它会扩展为对 my_dot 的有效调用)。一般来说,您可能希望编写一个 SWIG 宏来执行此任务。

其他情况

在您遇到其他将有帮助的包装情况时,numpy.i 可能也会有所帮助。

  • 在某些情况下,您可以使用 %numpy_typemaps 宏为自己的类型实现类型映射。请参见 其他常见类型:bool 或 其他常见类型:complex 部分的示例。另一种情况是,如果您的维度不是 int 类型(例如,是 long 类型):

  • %numpy_typemaps(double, NPY_DOUBLE, long) 
    
  • 您可以使用 numpy.i 中的代码编写自己的类型映射。例如,如果您有一个五维数组作为函数参数,您可以将适当的四维类型映射剪切并粘贴到接口文件中。对于第四个维度的修改将是微不足道的。

  • 有时,最佳方法是使用 %extend 指令为您的类定义新方法(或重载现有方法),这些方法接受一个 PyObject*(可以是或可以转换为 PyArrayObject*)而不是指向缓冲区的指针。在这种情况下,numpy.i 中的辅助例程非常有用。

  • 编写类型映射可能有点不直观。如果您对为 NumPy 编写 SWIG 类型映射有具体问题,numpy.i 的开发人员会监视 Numpy-discussion 和 Swig-user 邮件列表。

最后要注意的一点

当您使用 %apply 指令时,通常需要使用 numpy.i,它将一直生效,直到您告诉 SWIG 不要这样做为止。如果您要包装的函数或方法的参数具有常见名称,例如 lengthvector,这些类型映射可能会在您意外或不希望的情况下应用。因此,建议在完成特定类型映射后始终添加 %clear 指令:

%apply (double* IN_ARRAY1, int DIM1) {(double* vector, int length)}
%include "my_header.h"
%clear (double* vector, int length); 

一般来说,您应该特别指定这些类型映射签名的目标位置,然后在完成后将其清除。

一个常见示例

考虑一个合理的点积函数原型:

double dot(int len, double* vec1, double* vec2); 

我们希望得到的 Python 接口是:

def dot(vec1, vec2):
  """
 dot(PyObject,PyObject) -> double
 """ 

这里的问题在于有一个维度参数和两个数组参数,而我们的类型映射是设置为应用于单个数组的维度(实际上,SWIG没有提供将lenvec2关联到接受两个 Python 输入参数的机制)。推荐的解决方案如下:

%apply (int DIM1, double* IN_ARRAY1) {(int len1, double* vec1),
                                      (int len2, double* vec2)}
%rename (dot) my_dot;
%exception my_dot {
    $action
    if (PyErr_Occurred()) SWIG_fail;
}
%inline %{
double my_dot(int len1, double* vec1, int len2, double* vec2) {
    if (len1 != len2) {
        PyErr_Format(PyExc_ValueError,
                     "Arrays of lengths (%d,%d) given",
                     len1, len2);
        return 0.0;
    }
    return dot(len1, vec1, vec2);
}
%} 

如果包含double dot()原型的头文件还包含您想要封装的其他原型,因此需要%include此头文件,则在%rename之后和%include之前还需要一个%ignore dot;指令。或者,如果涉及的函数是类方法,则除了使用%inline外,还需要使用%extend而不是%ignore

关于错误处理的说明:请注意,my_dot返回一个double,但也可能引发 Python 错误。结果包装函数将在向量长度不匹配时返回 Python 浮点数表示的 0.0。由于这不是NULL,Python 解释器将不知道要检查错误。因此,我们在my_dot上方添加了%exception指令,以获取我们想要的行为(请注意$action是一个宏,展开为对my_dot的有效调用)。一般情况下,您可能需要编写SWIG宏来执行此任务。

其他情况

在其他封装情况下,当遇到时,可能需要使用numpy.i

  • 在某些情况下,您可以使用%numpy_typemaps宏为自己的类型实现类型映射。查看其他常见类型:bool 或其他常见类型:复数部分获取示例。另一种情况是,如果您的维度不是int类型(例如long):

  • %numpy_typemaps(double, NPY_DOUBLE, long) 
    
  • 您可以使用numpy.i中的代码编写自己的类型映射。例如,如果您有一个作为函数参数的五维数组,您可以将适当的四维类型映射剪切并粘贴到您的接口文件中。对于第四维度的修改将是微不足道的。

  • 有时候,最好的方法是使用%extend指令为您的类定义新方法(或重载现有方法),该方法接受PyObject*(既是或能够转换为PyArrayObject*)而不是指向缓冲区的指针。在这种情况下,numpy.i中的辅助程序非常有用。

  • 写作类型映射可能有点不直观。如果您对为 NumPy 编写SWIG类型映射有具体问题,numpy.i的开发人员会监视 Numpy-discussion 和 Swig-user 邮件列表。

最后说明

当你使用 %apply 指令时,通常需要使用 numpy.i,它将保持有效,直到你告诉 SWIG 不再需要。如果你要包装的函数或方法的参数具有常见的名称,比如 lengthvector,这些 typemap 可能会应用在你意料之外或不希望的情况下。因此,始终将 %clear 指令添加到特定 typemap 完成后是个好主意:

%apply (double* IN_ARRAY1, int DIM1) {(double* vector, int length)}
%include "my_header.h"
%clear (double* vector, int length); 

一般来说,你应该将这些 typemap 签名专门定位到你想要的位置,然后在完成后清除它们。

摘要

numpy.i默认提供支持在 NumPy 数组和 C 数组之间转换的 typemaps:

  • 这可以是 12 种不同的标量类型:signed charunsigned charshortunsigned shortintunsigned intlongunsigned longlong longunsigned long longfloatdouble
  • 支持每个数据类型的 74 种不同参数签名,包括:
    • 一维、二维、三维和四维数组。
    • 输入唯一、原地、argout、argoutview 和内存管理的 argoutview 行为。
    • 硬编码的维度、数据缓冲区-维度规范和维度-数据缓冲区规范。
    • 2D、3D 和 4D 数组的 C 排序(“最后维度最快”)或 Fortran 排序(“第一维度最快”)支持。

numpy.i接口文件还为包装开发人员提供了其他工具,包括:

  • 一个 SWIG 宏(%numpy_typemaps),有三个参数,用于为用户选择的 C 数据类型、NumPy 数据类型(假设它们匹配)和维度类型实现 74 个参数签名。
  • 十四个 C 宏和十五个 C 函数,可用于编写处理提供的 typemaps 未涵盖情况的专用 typemap、扩展或内联函数。请注意,这些宏和函数专门编码以与 NumPy C/API 一起使用,而不管 NumPy 版本号如何,包括在版本 1.6 之后一些 API 的弃用后。

测试 numpy.i 类型映射

原文:numpy.org/doc/1.26/reference/swig.testing.html

介绍

numpy.i SWIG 接口文件编写测试是一种组合性的头痛。目前,支持 12 种不同的数据类型,每种类型有 74 种不同的参数签名,总共支持 888 个类型映射“开箱即用”。每个类型映射可能需要多个单元测试来验证预期行为,无论是对正确还是不正确的输入。目前,在 numpy/tools/swig 子目录中运行 make test 时会执行超过 1,000 个单独的单元测试。

为了简化这些相似的单元测试,采用了一些高级编程技术,包括 C 和 SWIG 宏,以及 Python 继承。本文档的目的是描述用于验证 numpy.i 类型映射是否按预期工作的测试基础设施。

测试组织

支持三个独立的测试框架,分别用于一维、二维和三维数组。对于一维数组,有两个 C++ 文件,一个头文件和一个源文件,分别命名为:

Vector.h
Vector.cxx 

包含原型和代码的文件,其中有多种函数,其函数参数为一维数组。该文件为:

Vector.i 

是一个 SWIG 接口文件,定义了一个名为 Vector 的 Python 模块,用于包装 Vector.h 中的函数,同时利用 numpy.i 中的类型映射正确处理 C 数组。

Makefile 调用 swig 来生成 Vector.pyVector_wrap.cxx,并执行 setup.py 脚本来编译 Vector_wrap.cxx 并链接扩展模块 _Vector.so_Vector.dylib,取决于平台。这个扩展模块和代理文件 Vector.py 都放在 build 目录下的子目录中。

实际测试是通过名为 Python 脚本执行的:

testVector.py 

使用标准 Python 库模块 unittest 编写,该模块为 Vector.h 中定义的每个函数的每种支持的数据类型执行多个测试。

二维数组的测试方式完全相同。上述描述同样适用,只是将 Vector 替换为 Matrix。对于三维测试,将 Vector 替换为 Tensor。对于四维测试,将 Vector 替换为 SuperTensor。对于平坦的原位数组测试,将 Vector 替换为 Flat。对于接下来的描述,我们将引用 Vector 测试,但相同的信息也适用于 MatrixTensorSuperTensor 测试。

命令 make test 将确保构建所有测试软件,然后运行所有三个测试脚本。

测试头文件

Vector.h 是一个 C++ 头文件,定义了一个名为TEST_FUNC_PROTOS的 C 宏,它接受两个参数:TYPE,这是一个数据类型名称,比如unsigned int;和SNAME,这是没有空格的同一数据类型的简称,例如uint。此宏定义了几个具有前缀SNAME的函数原型,并且至少有一个参数是TYPE类型的数组。那些有返回参数的函数返回一个TYPE值。

TEST_FUNC_PROTOS 然后还针对numpy.i所支持的所有数据类型进行了实现:

  • signed char
  • unsigned char
  • short
  • unsigned short
  • int
  • unsigned int
  • long
  • unsigned long
  • long long
  • unsigned long long
  • float
  • double

测试源文件

Vector.cxx 是一个 C++ 源文件,实现了Vector.h中指定的每个函数原型的可编译代码。它定义了一个 C 宏 TEST_FUNCS,其参数和Vector.h中的TEST_FUNC_PROTOS的工作方式相同。TEST_FUNCS针对上述的 12 种数据类型实现。

测试 SWIG 接口文件

Vector.i 是一个SWIG 接口文件,定义了 python 模块Vector。它遵循使用numpy.i的惯例,如本章所述。它定义了一个SWIG%apply_numpy_typemaps,它有一个名为TYPE的单参数。它使用SWIG指令%apply,将提供的 typemap 应用于Vector.h中发现的参数签名。然后,它为numpy.i支持的所有数据类型实现了这个宏。然后它通过%include "Vector.h"来使用numpy.i中的 typemap 包装Vector.h中的所有函数原型。

测试 Python 脚本

使用make构建测试扩展模块后,可以运行testVector.py来执行测试。与其他使用unittest来进行单元测试的脚本一样,testVector.py定义了一个从unittest.TestCase继承的类:

class VectorTestCase(unittest.TestCase): 

然而,此类不会直接运行。它作为几个其他 python 类的基类,每个类都专门针对特定的数据类型。VectorTestCase 类存储了两个字符串以供输入信息:

self.typeStr

一个与Vector.hVector.cxx中使用的SNAME前缀之一匹配的字符串。例如,"double"

self.typeCode

一个短字符串(通常是单字符),代表 numpy 中的数据类型,并对应于self.typeStr。例如,如果self.typeStr"double",那么self.typeCode应该是"d"

VectorTestCase 类定义的每个测试通过访问Vector模块的字典来提取其尝试测试的 python 函数:

length = Vector.__dict__[self.typeStr + "Length"] 

对于双精度测试,这将返回 python 函数Vector.doubleLength

然后我们针对每种支持的数据类型定义了一个新的测试用例类,其定义如下:

class doubleTestCase(VectorTestCase):
    def __init__(self, methodName="runTest"):
        VectorTestCase.__init__(self, methodName)
        self.typeStr  = "double"
        self.typeCode = "d" 

这 12 个类都被收集到一个unittest.TestSuite中,然后执行。错误和失败被汇总并作为退出参数返回。任何非零结果表示至少一个测试未通过。

介绍

numpy.i SWIG接口文件编写测试是一个组合头痛的问题。目前支持 12 种不同数据类型,每种数据类型有 74 个不同的参数签名,总共支持 888 个类型映射“开箱即用”。每个类型映射可能需要多个单元测试,以验证对于正确和不正确的输入,行为是否符合预期。当前,当在numpy/tools/swig子目录中运行make test时,会执行超过 1,000 个单独的单元测试。

为了简化这些类似的单元测试,采用了一些高级编程技术,包括 C 和SWIG宏,以及 Python 继承。本文档的目的是描述用于验证numpy.i类型映射是否按预期工作的测试基础设施。

测试组织

支持三种独立的测试框架,分别针对一维、二维和三维数组。对于一维数组,有两个 C++文件,一个头文件和一个源文件,命名为:

Vector.h
Vector.cxx 

包含原型和代码的头文件,其中有各种函数,这些函数以一维数组作为函数参数。该文件为:

Vector.i 

是一个SWIG接口文件,定义了一个包装Vector.h中函数的 python 模块Vector,同时利用numpy.i中的类型映射来正确处理 C 数组。

Makefile调用swig生成Vector.pyVector_wrap.cxx,还执行setup.py脚本编译Vector_wrap.cxx,并链接扩展模块_Vector.so_Vector.dylib,取决于平台。这个扩展模块和代理文件Vector.py都放在build目录下的一个子目录中。

实际测试是通过名为 Python 脚本进行的:

testVector.py 

使用标准 Python 库模块unittest,对Vector.h中定义的每个函数进行多个测试,支持多种数据类型。

二维数组以完全相同的方式进行测试。上述描述适用,但用Matrix替换Vector。对于三维测试,用Tensor替换Vector。对于四维测试,用SuperTensor替换Vector。对于平面原地数组测试,将Flat替换为Vector。在接下来的描述中,我们将参考Vector测试,但相同信息也适用于MatrixTensorSuperTensor测试。

命令make test将确保构建所有测试软件,然后运行所有三个测试脚本。

测试头文件

Vector.h 是一个 C++ 头文件,定义了一个名为 TEST_FUNC_PROTOS 的 C 宏,它有两个参数:TYPE,这是一个数据类型名称,例如 unsigned int;以及 SNAME,这是没有空格的相同数据类型的简短名称,例如 uint。这个宏定义了几个以 SNAME 为前缀的函数原型,这些函数至少有一个参数是 TYPE 类型的数组。这些函数中有返回参数的函数返回一个 TYPE 值。

TEST_FUNC_PROTOS 接着为 numpy.i 支持的所有数据类型实现:

  • signed char(有符号字符)
  • unsigned char(无符号字符)
  • short(短整数)
  • unsigned short(无符号短整数)
  • int(整数)
  • unsigned int(无符号整数)
  • long(长整数)
  • unsigned long(无符号长整数)
  • long long(长长整数)
  • unsigned long long(无符号长长整数)
  • float(单精度浮点数)
  • double(双精度浮点数)

测试源文件

Vector.cxx 是一个 C++ 源文件,为 Vector.h 中指定的每个函数原型实现可编译代码。它定义了一个 C 宏 TEST_FUNCS,它与 Vector.h 中的 TEST_FUNC_PROTOS 的参数相同,且工作方式也相同。TEST_FUNCS 为上面提到的 12 种数据类型进行了实现。

测试 SWIG 接口文件

Vector.i 是一个 SWIG 接口文件,定义了 python 模块 Vector。它遵循本章中描述的使用 numpy.i 的约定。它定义了一个 SWIG%apply_numpy_typemaps,它有一个参数 TYPE。它使用 SWIG 指令 %apply 将提供的类型映射应用于 Vector.h 中找到的参数签名。这一宏随后对 numpy.i 支持的所有数据类型进行实现。接着,它执行 %include "Vector.h" 来使用 numpy.i 中的类型映射包装 Vector.h 中的所有函数原型。

测试 Python 脚本

在使用 make 构建测试扩展模块后,可以运行 testVector.py 来执行测试。与其他使用 unittest 进行单元测试的脚本一样,testVector.py 定义了一个继承自 unittest.TestCase 的类:

class VectorTestCase(unittest.TestCase): 

然而,这个类不会直接运行。而是作为几个其他 python 类的基类,每个类都特定于一种特定的数据类型。VectorTestCase 类存储了两个用于类型信息的字符串:

self.typeStr

一个匹配 Vector.hVector.cxx 中使用的 SNAME 前缀之一的字符串。例如,"double"

self.typeCode

一个表示 numpy 中数据类型的短字符串(通常是单个字符),对应于 self.typeStr。例如,如果 self.typeStr"double",那么 self.typeCode 应该是 "d"

VectorTestCase 类定义的每个测试通过访问 Vector 模块的字典提取它尝试测试的 python 函数:

length = Vector.__dict__[self.typeStr + "Length"] 

在双精度测试的情况下,这将返回 python 函数 Vector.doubleLength

然后,我们为每个支持的数据类型定义一个新的测试用例类,定义如下:

class doubleTestCase(VectorTestCase):
    def __init__(self, methodName="runTest"):
        VectorTestCase.__init__(self, methodName)
        self.typeStr  = "double"
        self.typeCode = "d" 

每个这 12 个类都被收集到一个unittest.TestSuite中,然后被执行。错误和失败会被汇总在一起并作为退出参数返回。任何非零结果都表示至少有一个测试未通过。

贡献给 NumPy

原文:numpy.org/doc/1.26/dev/index.html

不会编码?没问题!NumPy 是多方面的,我们可以使用大量帮助。这些都是我们需要帮助的活动(它们都很重要,所以我们按字母顺序列出):

  • 代码维护和开发

  • 社区协调

  • 开发运维

  • 制作教育内容和叙述文档

  • 筹款

  • 市场营销

  • 项目管理

  • 翻译内容

  • 网站设计和开发

  • 撰写技术文档

本文的其余部分讨论了在 NumPy 代码库和文档上的工作。我们正在更新我们对其他活动和角色的描述。如果您对这些其他活动感兴趣,请与我们联系!您可以通过 numpy-discussion 邮件列表GitHub(提出问题或评论相关问题)联系我们!这是我们首选的交流渠道(开源天性就是开放的!),但是如果您希望首先私下讨论,请联系我们的社区协调员 numpy-team@googlegroups.com 或 numpy-team.slack.com (第一次请发送电子邮件至 numpy-team@googlegroups.com 请求邀请)。

开发进程 - 摘要

这是简短摘要,完整的目录链接如下:

  1. 如果您是首次贡献者:

    • 前往 github.com/numpy/numpy 并单击“fork”按钮来创建您自己的项目副本。

    • 将项目克隆到本地计算机:

      git clone --recurse-submodules https://github.com/your-username/numpy.git 
      
    • 更改目录:

      cd numpy 
      
    • 添加上游仓库:

      git remote add upstream https://github.com/numpy/numpy.git 
      
    • 现在,git remote -v 将显示两个名为远程仓库:

      • upstream,指的是 numpy 仓库

      • origin,指的是您个人的 fork

    • 从上游拉取最新更改,包括标签:

      git checkout main
      git pull upstream main --tags 
      
    • 初始化 numpy 的子模块:

      git submodule update --init 
      
  2. 开发您的贡献:

    • 为要处理的功能创建一个分支。由于分支名称将出现在合并消息中,请使用一个合理的名称,如 'linspace-speedups':

      git checkout -b linspace-speedups 
      
    • 随着您的进展本地提交(git addgit commit)使用 格式良好的 提交消息,编写在您的更改之前和之后失败的测试,并在本地运行所有测试。确保在文档字符串中记录任何更改的行为,严格遵守 NumPy 文档字符串标准。

  3. 提交您的贡献:

    • 将更改推送回您在 GitHub 上的 fork:

      git push origin linspace-speedups 
      
    • 输入您的 GitHub 用户名和密码(重复贡献者或高级用户可以通过连接到 GitHub 使用 SSH 来删除此步骤)。

    • 转到 GitHub。新分支将显示为绿色的拉取请求按钮。确保标题和消息清晰、简洁,并且自解释。然后点击按钮提交它。

    • 如果您的提交引入了新功能或更改了功能,请在邮件列表上解释您的更改。对于错误修复、文档更新等,一般来说是不必要的,不过如果您没有得到任何反应,可以随时要求审查。

  4. 审查流程:

    • 审阅者(其他开发人员和感兴趣的社区成员)将就您的拉取请求(PR)撰写内联和/或一般评论,以帮助您改善其实施、文档和风格。项目中的每位开发人员都会经过代码审查,我们已经把这视为友好的对话,从中我们都会学到东西,整体代码质量也会受益。因此,请不要让审查使您不愿意贡献:它的唯一目的是改善项目的质量,而不是批评(毕竟,我们对您捐赠的时间非常感激!)。更多信息请参见我们的审查者指南。

    • 要更新您的 PR,请在本地存储库上进行更改,提交,运行测试,仅在测试通过后将更改推送到您的分支上。一旦这些更改被推送上去(到之前的相同分支),PR 将自动更新。如果您不知道如何修复测试失败,您可以无论如何推送您的更改,并在 PR 评论中寻求帮助。

    • 每次 PR 更新后,各种持续集成(CI)服务都会被触发,用于构建代码、运行单元测试、衡量代码覆盖率和检查您分支的编码风格。在您的 PR 可以合并之前,CI 测试必须通过。如果 CI 失败,您可以通过点击“失败”图标(红色叉号)并检查构建和测试日志来找出失败的原因。为了避免过度使用和浪费这一资源,请在提交之前本地测试您的工作。

    • 在合并之前,PR 必须得到至少一位核心团队成员的批准。批准意味着核心团队成员仔细审查了更改,并且 PR 已经准备好合并。

  5. 文档更改

    除了对函数的文档字符串进行更改和一般文档中的描述之外,如果您的更改引入了任何面向用户的修改,它们可能需要在发布说明中提及。要添加您的更改到发布说明中,您需要创建一个简短的文件,并放置在doc/release/upcoming_changes中。文件doc/release/upcoming_changes/README.rst详细说明了格式和文件名约定。

    如果您的更改引入了弃用,请确保首先在 GitHub 或邮件列表上讨论此事。如果就弃用达成协议,请遵循NEP 23 弃用政策 添加弃用。

  6. 交叉引用问题

    如果 PR 与任何问题相关,则可以将文本 xref gh-xxxx 添加到 GitHub 评论中,其中 xxxx 是问题编号。同样,如果 PR 解决了一个问题,用 closesfixes 或其他任何 GitHub 接受的 变体 替换 xref

    在源代码中,请务必在任何问题或 PR 引用之前加上 gh-xxxx

要了解更详细的讨论,请继续阅读并关注本页面底部的链接。

upstream/main 和您的 feature 分支之间的分歧

如果 GitHub 指示您的 Pull Request 分支无法再自动合并,则必须将自您开始以来所做的更改合并到您的分支中。我们建议的做法是在 main 上变基。

指南

  • 所有代码都应该有测试(有关更多详细信息,请参见下文的 test coverage)。

  • 所有代码都应被文档化

  • 没有经核心团队成员审查和批准的更改会被提交。如果您的拉取请求一周内没有任何回应,请礼貌地在 PR 上或邮件列表上询问。 ### 风格指南

  • 设置您的编辑器遵循PEP 8(去除末尾空格,不使用制表符等)。使用 pyflakes / flake8 检查代码。

  • 使用 NumPy 数据类型而不是字符串(np.uint8 而不是 "uint8")。

  • 使用以下的导入约定:

    import numpy as np 
    
  • 对于 C 代码,请参阅NEP 45

测试覆盖率

修改代码的拉取请求(PRs)应该有新的测试,或修改现有测试以确保在 PR 之前失败后通过。在推送 PR 之前,应运行测试。

在本地运行 NumPy 的测试套件需要一些额外的包,如 pytesthypothesis。其他测试依赖项列在顶级目录中的 test_requirements.txt 中,并可以通过以下命令方便地安装:

$ python -m pip install -r test_requirements.txt 

模块的测试理想情况下应覆盖该模块中的所有代码,即语句覆盖率应为 100%。

要测量测试覆盖率,请运行:

$ spin test --coverage 

这将在 build/coverage 中以 html 格式创建报告,可用浏览器查看,例如:

$ firefox build/coverage/index.html 

构建文档

要构建 HTML 文档,请使用:

spin docs 

您还可以从 doc 目录运行 makemake help 列出所有目标。

要获取适当的依赖项和其他要求,请参阅构建 NumPy API 和参考文档。

修复警告

  • “找不到引用:R###”可能是在 docstring 的第一行后有一个下划线引用(例如[1]_)。使用以下方法查找源文件:$ cd doc/build; grep -rin R####

  • “重复引用 R###,其他实例在……”可能有一个[2]没有一个[1]在其中一个文档字符串中

开发过程 - 详细信息

故事的剩余部分

  • Git 基础知识

    • 安装 git

    • 获取代码的本地副本

    • 更新代码

    • 为 NumPy 开发设置 git

    • Git 配置

    • 差异规范中的两个和三个点

    • 其他 Git 资源

  • 设置和使用开发环境

    • 推荐的开发设置

    • 使用虚拟环境

    • 测试构建

    • 其他构建选项

    • 运行测试

    • 运行 Linting

    • 重建和清理工作区

    • 调试

    • 了解代码和入门

  • 构建 NumPy API 和参考文档

    • 开发环境

    • 先决条件

    • 说明

  • 开发工作流程

    • 基本工作流程

    • 可能还想做的其他事情

  • 高级调试工具

    • 使用其他工具查找 C 错误
  • 审稿人指南

    • 谁可以成为审稿人?

    • 沟通准则

    • 审稿人清单

    • 审稿时的标准回复

  • NumPy 基准测试

    • 用法

    • 基准测试版本

    • 编写基准测试

  • NumPy C 样式指南

  • 面向下游软件包作者

    • 了解 NumPy 的版本控制和 API/ABI 稳定性

    • 针对 NumPy 主分支或预发布版本进行测试

    • 添加对 NumPy 的依赖

  • 发布版本

    • 如何准备发布

    • 逐步说明

    • 分支演示

  • NumPy 治理

    • NumPy 项目治理和决策
  • 如何贡献到 NumPy 文档

    • 文档团队会议

    • 所需内容

    • 贡献修复

    • 贡献新页面

    • 间接贡献

    • 文档样式

    • 阅读文档

NumPy 特定的工作流程在 numpy 开发工作流程中。

开发流程 - 总结

这是简要摘要,完整的 TOC 链接在下面:

  1. 如果您是首次贡献者:

    • 转到github.com/numpy/numpy并单击“fork”按钮以创建项目的自己的副本。

    • 在本地计算机上克隆项目:

      git clone --recurse-submodules https://github.com/your-username/numpy.git 
      
    • 更改目录:

      cd numpy 
      
    • 添加上游存储库:

      git remote add upstream https://github.com/numpy/numpy.git 
      
    • 现在,git remote -v将显示两个名为的远程存储库:

      • 上游,指的是numpy存储库

      • origin,指的是您的个人分支

    • 从上游拉取最新的更改,包括标签:

      git checkout main
      git pull upstream main --tags 
      
    • 初始化 numpy 的子模块:

      git submodule update --init 
      
  2. 开发您的贡献:

    • 为您想要处理的功能创建一个分支。由于分支名称将出现在合并消息中,请使用合理的名称,例如'linspace-speedups':

      git checkout -b linspace-speedups 
      
    • 在进展中本地提交(git addgit commit)使用正确的格式提交消息,编写在更改之前和更改之后失败的测试,并在本地运行所有测试。确保在文档字符串中记录任何更改的行为,遵守 NumPy 文档字符串标准。

  3. 提交您的贡献:

    • 将您的更改推送回 GitHub 上的个人分支:

      git push origin linspace-speedups 
      
    • 输入您的 GitHub 用户名和密码(重复贡献者或高级用户可以通过使用 SSH 连接到 GitHub 来删除此步骤)。

    • 转到 GitHub。新分支将显示一个绿色的 Pull Request 按钮。请确保标题和消息清晰、简洁且自解释。然后点击按钮提交。

    • 如果您的提交引入新功能或更改功能,请在邮件列表上解释您的更改。对于错误修复、文档更新等,通常不需要这样做,但如果您没有得到任何反应,请随时要求审查。

  4. 审查流程:

    • 评审人员(其他开发人员和感兴趣的社区成员)将对您的 Pull Request(PR)编写行内和/或常规评论,以帮助您改进其实现、文档和风格。项目中的每个开发人员都要进行代码审查,我们认为这是友好对话,我们都从中学到了东西,并且整体的代码质量得到了提高。因此,请不要让审查使您不敢贡献:它的唯一目的是提高项目的质量,而不是批评(我们非常感谢您愿意捐赠时间!)。有关更多信息,请参阅我们的评审人准则。

    • 要更新您的 PR,在本地仓库上进行更改,提交,运行测试,并且只有测试通过时才推送到您的分支。当这些更改被推送上去(到与之前相同的分支上)时,PR 将自动更新。如果对于如何修复测试失败没有任何想法,您仍然可以推送更改并在 PR 评论中寻求帮助。

    • 每次 PR 更新后,会触发各种持续集成(CI)服务来构建代码,运行单元测试,测量代码覆盖率和检查分支的编码风格。在合并您的 PR 之前,CI 测试必须通过。如果 CI 失败,您可以点击“失败”图标(红叉)查看构建和测试日志,找出失败的原因。为了避免滥用和浪费这些资源,在提交之前,请在本地进行测试工作。

    • 在合并之前,PR 必须由至少一个核心团队成员批准。批准意味着核心团队成员仔细审查了更改,并且 PR 已经准备好进行合并。

  5. 文档更改

    除了对函数 docstring 的更改和总体文档中的可能描述之外,如果您的更改引入了任何面向用户的修改,可能需要在发布说明中提到。要将您的更改添加到发布说明中,您需要创建一个简短的文件概述,并将其放置在doc/release/upcoming_changes目录中。文件doc/release/upcoming_changes/README.rst详细说明了格式和文件名约定。

    如果您的更改引入了废弃,请确保首先在 GitHub 或邮件列表上讨论。如果就废弃达成一致意见,请遵循NEP 23 废弃政策 添加废弃。

  6. 交叉引用问题

    如果 PR 涉及任何问题,您可以将文本xref gh-xxxx添加到 GitHub 评论中,其中xxxx是问题的编号。同样,如果 PR 解决了一个问题,将xref替换为closesfixes或其他GitHub 接受的形式

    在源代码中,务必在任何问题或 PR 引用前加上gh-xxxx

要进行更详细的讨论,请继续阅读并关注本页面底部的链接。

“上游/主”与您的特性分支之间的分歧

如果 GitHub 指示无法自动合并您拉取请求的分支,则必须将自您开始以来发生的更改合并到您的分支中。我们建议的处理方式是在主分支上变基。

指导方针

  • 所有代码都应该有测试(有关更多详细信息,请参见 test coverage)。

  • 所有代码都应该有文档记录

  • 任何更改在未经核心团队成员审查和批准之前不能提交。如果在一周内对您的拉取请求没有响应,请在 PR 或者邮件列表上礼貌地询问。 ### 风格指南

  • 设置编辑器遵循PEP 8(删除尾随空格、无制表符等)。使用 pyflakes / flake8 检查代码。

  • 使用 NumPy 数据类型而不是字符串(np.uint8代替"uint8")。

  • 使用以下导入约定:

    import numpy as np 
    
  • 对于 C 代码,参见NEP 45

测试覆盖率

修改代码的拉取请求(PR)应该要么有新的测试,要么修改现有测试以在 PR 之前失败,在 PR 之后通过。在推送 PR 之前,您应该运行测试。

在本地运行 NumPy 的测试套件需要一些额外的包,如pytesthypothesis。额外的测试依赖列在顶层目录下的test_requirements.txt中,并可通过以下方式方便地安装:

$ python -m pip install -r test_requirements.txt 

测试一个模块应该尽可能覆盖该模块中的所有代码,即语句覆盖率应达到 100%。

要测量测试覆盖率,请运行:

$ spin test --coverage 

这将在build/coverage目录下生成一个html格式的报告,可以用浏览器查看,例如:

$ firefox build/coverage/index.html 

构建文档

要构建 HTML 文档,请使用:

spin docs 

您也可以在doc目录下运行makemake help列出所有目标。

要获取适当的依赖项和其他要求,请参阅构建 NumPy API 和参考文档。

修复警告

  • “找不到引用:R###” 可能是在文档字符串的第一行引用后面有下划线(例如 [1]_)。使用以下方法查找源文件:$ cd doc/build; grep -rin R####

  • “重复引用 R###,其他实例在…”” 可能是一个文档字符串中的 [2] 没有 [1]

upstream/main和你的特性分支之间的分歧

如果 GitHub 指示你的拉取请求的分支无法自动合并,你必须将自己分支中进行的更改合并到主分支中。我们建议的方法是在主分支上变基。

指南

  • 所有代码都应该有测试(有关更多详细信息,请参阅测试覆盖率)。

  • 所有代码都应该有文档

  • 任何更改都不会在核心团队成员审查并批准之前提交。如果在一个星期内没有回应你的拉取请求,请在 PR 或者邮件列表上礼貌地询问。

风格指南

  • 设置你的编辑器遵循PEP 8(去除尾随空格,无制表符等)。使用 pyflakes / flake8 检查代码。

  • 使用 NumPy 数据类型而不是字符串(np.uint8而不是"uint8")。

  • 使用以下导入约定:

    import numpy as np 
    
  • 对于 C 代码,请参阅NEP 45(“在 NumPy 增强提案中”)。

测试覆盖率

修改代码的拉取请求(PRs)应该有新测试,或者修改现有测试在 PR 之前失败后成功。在推送 PR 之前,你应该运行测试。

本地运行 NumPy 的测试套件需要一些额外的软件包,例如pytesthypothesis。额外的测试依赖项列在顶级目录的test_requirements.txt中,可以通过以下方式方便地安装:

$ python -m pip install -r test_requirements.txt 

模块的测试应该尽可能覆盖该模块中的所有代码,即语句覆盖率应达到 100%。

要测量测试覆盖率,请运行:

$ spin test --coverage 

这将在build/coverage目录中创建一个html格式的报告,可以在浏览器中查看,例如:

$ firefox build/coverage/index.html 

构建文档

要构建 HTML 文档,请使用:

spin docs 

你也可以在doc目录中运行make命令。make help列出所有目标。

要获取适当的依赖项和其他要求,请参阅构建 NumPy API 和参考文档。

修复警告

  • “找不到引用:R###” 可能是在文档字符串的第一行引用后面有下划线(例如 [1]_)。使用以下方法查找源文件:$ cd doc/build; grep -rin R####

  • “重复引用 R###,其他实例在…” 可能有一个 [2] 而没有 [1] 在其中的一个文档字符串中

修复警告

  • “引用未找到:R###” 第一行的文档字符串中可能有一个引用后面有下划线 (e.g. [1]_)。使用以下方法来找到源文件:$ cd doc/build; grep -rin R####

  • “重复引用 R###,其他实例在…” 可能有一个 [2] 而没有 [1] 在其中的一个文档字符串中

开发流程 - 详细信息

故事的余下部分

  • Git 基础知识

    • 安装 git

    • 获取代码的本地副本

    • 更新代码

    • 为 NumPy 开发设置 git

    • Git 配置

    • 差异规范中的两个和三个点

    • 其他 Git 资源

  • 设置和使用您的开发环境

    • 推荐的开发环境设置

    • 使用虚拟环境

    • 测试构建

    • 其他构建选项

    • 运行测试

    • 运行代码检查

    • 重建和清理工作区

    • 调试

    • 理解代码和入门

  • 构建 NumPy API 和参考文档

    • 开发环境

    • 先决条件

    • 说明

  • 开发工作流程

    • 基本工作流程

    • 您可能想要做的其他事情

  • 高级调试工具

    • 使用其他工具寻找 C 错误
  • 审核者指南

    • 谁可以成为审核者?

    • 沟通指南

    • 审核者清单

    • 审核的标准回复

  • NumPy 基准测试

    • 用法

    • 基准测试版本

    • 编写基准测试

  • NumPy C 代码风格指南

  • 针对下游包作者

    • 理解 NumPy 的版本和 API/ABI 稳定性

    • 针对 NumPy 主分支或预发布版本进行测试

    • 添加对 NumPy 的依赖

  • 发布一个版本

    • 如何准备发布

    • 逐步指南

    • 分支演示

  • NumPy 治理

    • NumPy 项目治理和决策
  • 如何贡献到 NumPy 文档

    • 文档团队会议

    • 所需内容

    • 贡献修正

    • 贡献新页面

    • 间接贡献

    • 文档风格

    • 文档阅读

NumPy 特定的工作流程在 numpy-development-workflow 中。

用于开发的 Git

原文:numpy.org/doc/1.26/dev/gitwash/index.html

这些页面描述了一般的 gitgithub 工作流程。

这不是一个全面的 git 参考。它是专门针对 github 托管服务的。您可能会发现有更好或更快的方法来完成 git 的工作,但这些应该可以让您开始。

关于学习 git 的一般资源,请参见其他 Git 资源。

请查看 github 帮助页面上可用的 github 安装帮助页面

目录:

  • 安装 git

  • 获取代码的本地副本

  • 更新代码

  • 为 NumPy 开发设置 git

    • 安装 git

    • 创建 GitHub 账户

    • 创建一个 NumPy 分叉

    • 查看一下

    • 可选:设置 SSH 密钥以避免密码

  • Git 配置

    • 概述

    • 详细信息

  • 差异规范中的两点和三点

  • 其他 Git 资源

    • 教程和总结

    • 高级 git 工作流程

    • 在线手册页

posted @ 2024-06-24 14:54  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报