三件事:用 devkit py-perf 采集 Python 火焰图定位热点;用 ctypes 在 ARM64 上安全加载 SO 库;借 openssl.cnf 让 Python 的 ssl 模块自动走 KAE——不改一行 Python 代码

Python 在鲲鹏上的处境

好消息:Python 本身、NumPy、Cython 这些主流工具在 aarch64 上都有成熟的支持。NEON 是 AArch64 的强制基线(第 1 讲讲过),所以 NumPy 默认就会用到 NEON——你在 x86 上写的 Python 数值代码,迁到鲲鹏基本不用改。

需要注意的部分:涉及底层 C 扩展、依赖特定硬件加速的场景,需要额外的接入工作。

一、用火焰图定位 Python 热点

Python 的性能问题常常不在你想的地方——一个不起眼的字符串拼接、一次意外的深拷贝,可能才是瓶颈。鲲鹏 DevKit 提供的 devkit py-perf 通过 ptrace 采样分析调用栈,输出 Top20 热点函数并绘制火焰图。

安装 DevKit

# RPM 方式(框架包必须先装,再装功能包)
sudo rpm -ivh devkit-x.x.x-1.aarch64.rpm \
              devkit-tuner-x.x.x-1.aarch64.rpm \
              devkit-porting-x.x.x-1.aarch64.rpm

# 验证
rpm -qa | grep devkit
devkit version

# 启用命令补全(可选)
source /etc/bash_completion.d/devkit.sh

如果用的是压缩包形式(DevKit-CLI-x.x.x-Linux-Kunpeng.tar.gz),解压后进目录用 ./devkit 调用。

⚠️ 不要把 devkit 二进制拷贝到别处——它会破坏依赖关系,导致工具无法运行。

采集火焰图

先准备一个"有热点"的 Python 脚本:

#!/usr/bin/env python3
"""
py_hotspot.py —— 一个包含明显热点的 Python 脚本,用于演示火焰图采集

用法:
    python3 py_hotspot.py          # 前台运行,供 py-perf 附加
"""
import time


def slow_string_concat(n):
    """反模式:用 += 拼接字符串(每次创建新对象)"""
    result = ""
    for i in range(n):
        result += str(i)
    return result


def fast_string_join(n):
    """正确做法:用 join(一次性分配)"""
    return "".join(str(i) for i in range(n))


def compute_heavy(n):
    """纯 Python 计算密集——GIL 下无法并行"""
    total = 0
    for i in range(n):
        total += (i * i) % 7
    return total


def main():
    while True:
        # 这三个函数构成主要热点
        s1 = slow_string_concat(20000)
        s2 = fast_string_join(20000)
        t = compute_heavy(500000)

        # 让 CPU 有喘息,便于采样
        time.sleep(0.05)

        # 打印一次就够,避免输出成为瓶颈
        if not hasattr(main, "printed"):
            print(f"len={len(s1)}, len={len(s2)}, total={t}")
            main.printed = True


if __name__ == "__main__":
    main()

后台运行它,拿到 PID:

python3 py_hotspot.py &
PY_PID=$!
echo "PID = $PY_PID"

采集:

devkit py-perf hotspot -p $PY_PID -d 5 -i 100 -o /home/demo/flamegraph -t --native

参数逐项说明:

参数含义
-p $PY_PID指定采集的进程 PID,仅支持单个进程,不采集子进程
-d 5采集时长 5 秒(最小 1s,默认一直采集)
-i 100采样间隔 100 毫秒(范围 1~1000ms,默认 10ms
-o /home/demo/flamegraph输出路径,生成 /home/demo/flamegraph.html
-t报告和火焰图按线程分组展示调用栈
--native同时采集 Python 和 C 的调用栈——不加则只采集 Python 栈

执行后输出开头是提示:

Press Ctrl+ \ to cancel the task or Ctrl+ C to stop the task collection and enter the analysis.

结束后会打印 Python Hotspot Top20 Summary Report 表格,以及:

The flamegraph html report: /home/demo/flamegraph.html

用浏览器打开这个 HTML,就能看到交互式火焰图。

看火焰图的三个要点

  1. 宽度 = 占用时间比例。横向越宽的方块,占用 CPU 时间越多。先看最宽的。
  2. slow_string_concat 应该明显比 fast_string_join——这直观印证了字符串拼接的反模式。
  3. --native 打开后能看到 C 层调用栈——如果热点落在 NumPy、正则、JSON 解析这些 C 扩展里,只有开了这个参数才看得见。

环境限制

⚠️ 硬性约束(官方明确):

  • Python 3.7.X ~ 3.11.X
  • 仅支持 openEuler
  • 仅支持 PID 模式(不能按可执行文件启动)

先检查版本:

python3 --version
cat /etc/os-release | head -2

二、用 ctypes 加载 ARM64 上的 SO 库

Python 调用 C 库的标准方式有两种:ctypes(标准库,无依赖)和 cffi(需要 pip 安装)。

ctypes 方式

#!/usr/bin/env python3
"""
ctypes_demo.py —— 用 ctypes 加载 ARM64 上的 SO 库

关键点:架构相关的类型宽度必须显式声明。
在 ARM64 上 long 是 8 字节(LP64),而 int 是 4 字节 —— 声明错了会读到垃圾数据。
"""
import ctypes
import platform

# 打印架构信息,确认我们在 64 位 ARM 上
print(f"machine: {platform.machine()}")     # 期望 aarch64
print(f"pointer size: {ctypes.sizeof(ctypes.c_void_p)} bytes")  # 期望 8
print(f"long size: {ctypes.sizeof(ctypes.c_long)} bytes")       # 期望 8(LP64)

# 加载标准 C 库(Linux 上通常是 libc.so.6)
# 用 CDLL 加载 C 风格的调用约定;Windows 上的 stdcall 对应 WinDLL
libc = ctypes.CDLL("libc.so.6")

# ── 示例:调用 malloc / free ──
# 关键:必须显式声明 argtypes 和 restype。
# ctypes 默认把返回值当 int(4 字节)处理,
# 而 malloc 返回的是 8 字节指针 —— 不声明的话,
# 64 位地址会被截断成 32 位,这是 ARM64 上最容易踩的 ctypes 坑!
libc.malloc.argtypes = [ctypes.c_size_t]
libc.malloc.restype = ctypes.c_void_p      # ← 必须显式声明为指针

libc.free.argtypes = [ctypes.c_void_p]
libc.free.restype = None

ptr = libc.malloc(1024)
print(f"malloc 返回: 0x{ptr:x}")             # 应该是 48 位的有效地址
assert ptr is not None and ptr != 0, "malloc 失败"

libc.free(ptr)
print("内存已释放")


# ── 示例:加载自定义 SO 并调用 ──
# 假设你编译了一个 libmycalc.so,导出 add_ints(int, int) -> int
try:
    mylib = ctypes.CDLL("./libmycalc.so")

    # 声明签名(同样是必须的)
    mylib.add_ints.argtypes = [ctypes.c_int, ctypes.c_int]
    mylib.add_ints.restype = ctypes.c_int

    result = mylib.add_ints(3, 4)
    print(f"add_ints(3, 4) = {result}")

except OSError as e:
    print(f"加载 libmycalc.so 失败(示例库可能不存在): {e}")

配套的 C 库源码:

/*
 * mycalc.c —— 供 Python ctypes 调用的示例库
 *
 * 编译(注意 -fPIC 是位置无关代码,共享库必需):
 *   gcc -O2 -fPIC -shared -o libmycalc.so mycalc.c
 */
int add_ints(int a, int b)
{
    return a + b;
}

编译并用 Python 调用:

gcc -O2 -fPIC -shared -o libmycalc.so mycalc.c
python3 ctypes_demo.py

ARM64 上的 ctypes 陷阱

最关键的一条:ctypes 默认假设返回值是 int(32 位)。

在 x86-64 和 AArch64 上指针都是 64 位,如果你的 C 函数返回指针、size_tlong不声明 restype 就会导致高 32 位被截断——表现为"地址看起来是有效的但一用就崩"。

C 类型正确的 ctypes 声明
intctypes.c_int
long(ARM64 上是 8 字节)ctypes.c_long
size_tctypes.c_size_t
指针ctypes.c_void_pctypes.POINTER(T)
结构体指针ctypes.POINTER(MyStruct)

第二:结构体要显式定义,注意对齐。

class Point(ctypes.Structure):
    _fields_ = [
        ("x", ctypes.c_double),
        ("y", ctypes.c_double),
    ]

ctypes 会按平台的 ABI 规则处理对齐,但如果你在 C 侧用了 __attribute__((packed)),Python 侧也要加 _pack_ = 1,否则字段偏移对不上。

⚠️ 说明:鲲鹏/openEuler 官方文档中没有 ctypes/cffi 的专门章节,本节内容属于通用 Python 知识。实机使用前建议先用 nm -D --defined-only yourlib.so | head 确认符号名真实存在。

三、让 Python 自动用上 KAE

通过第 11 讲的 openssl.cnf 配置,Python 的加密操作会自动走 KAE 硬件加速——Python 代码一个字都不用改

原理:Python 的 ssl 模块和 cryptography 库最终都调用 OpenSSL,配好 OpenSSL 引擎后底层自动切换。

# 配置环境变量(沿用第 11 讲创建的 openssl.cnf)
export OPENSSL_CONF=/home/app/openssl.cnf
export OPENSSL_ENGINES=/usr/local/lib/engines-1.1
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib

# 验证 OpenSSL 能看到引擎
openssl engine -t kae

写一个 Python 脚本验证加速是否生效:

#!/usr/bin/env python3
"""
ssl_kae_test.py —— 验证 Python 的加密操作是否走了 KAE 引擎

运行前先设置:
    export OPENSSL_CONF=/home/app/openssl.cnf
    export OPENSSL_ENGINES=/usr/local/lib/engines-1.1
"""
import ssl
import hashlib
import time


def bench_hash(algo: str, data: bytes, rounds: int = 2000) -> float:
    """测量哈希计算耗时(哈希走的是 OpenSSL 的 EVP 接口)"""
    t0 = time.perf_counter()
    for _ in range(rounds):
        h = hashlib.new(algo)
        h.update(data)
        h.digest()
    return (time.perf_counter() - t0) / rounds * 1e6   # 微秒


def main():
    # 打印 OpenSSL 版本信息 —— 确认运行时的 OpenSSL 是我们配置的那个
    print(f"OpenSSL 版本: {ssl.OPENSSL_VERSION}")

    data = b"x" * 4096

    # SM3 是 KAE 支持的国密算法(需 OpenSSL 支持)
    for algo in ["sha256", "sm3"]:
        try:
            us = bench_hash(algo, data)
            print(f"{algo:8s}: {us:8.2f} us/次")
        except ValueError:
            print(f"{algo:8s}: 当前 OpenSSL 不支持该算法")

    # 演示 TLS 上下文创建(真实的 TLS 握手会用到 KAE 的加解密加速)
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    print(f"TLS 上下文创建成功,可用密码套件 {len(ctx.get_ciphers())} 个")


if __name__ == "__main__":
    main()

运行两次对比——一次不带 KAE 配置,一次带上:

# 软件实现
unset OPENSSL_CONF
python3 ssl_kae_test.py

# KAE 加速
export OPENSSL_CONF=/home/app/openssl.cnf
export OPENSSL_ENGINES=/usr/local/lib/engines-1.1
python3 ssl_kae_test.py

判读:SM3 这类 KAE 支持的算法应该有明显加速;SHA256 可能没有(取决于 KAE 版本支持的算法集,见第 11 讲的算法矩阵)。

关于 NumPy 的说明

NumPy 在 ARM64 上默认就会用到 NEON(强制基线),不需要额外配置。

但不要指望 SVE 加速——鲲鹏 920 不支持 SVE(第 9 讲讲过,KSYS 文档明确 SVE/SME 数据采集仅部分新型号支持),面向标准 920 的优化应以 NEON 为目标。

如果你想确认 NumPy 实际用了什么指令,可以检查它的构建信息:

import numpy as np
np.show_config()

⚠️ 说明:鲲鹏/openEuler 官方文档中没有 NumPy 在 ARM64 上的专门优化指南,上述内容是通用知识。建议做法是:用第 6 讲的 KSYS 测量实际性能,用 devkit advisor vec-check 分析可向量化的代码片段,而不是假设某个库"应该"被优化了。

收尾:12 讲走过的路

第 1–3 讲:写了第一个 NEON 程序,记住"看起来更快 ≠ 实际更快";装了纳秒级秒表;看懂了机器的 NUMA 拓扑。

第 4–5 讲:把 SSE intrinsics 改成 NEON,也知道了什么时候该用 avx2ki 兼容层;复现并修复了一个"x86 能跑、ARM 挂掉"的并发 bug。

第 6–8 讲:用 KSYS 把"慢"变成具体的瓶颈假设;用 numactl 做 NUMA 绑定并用 numastat 验证;用 oeAware 把调优自动化。

第 9–12 讲:用 -fopt-info 和反汇编验证编译选项;走完 PGO 四步;用 KAE 接上硬件加速;最后是 Python 技术栈。

整套课只想留一句话的话,是第 1 讲就立下的那条纪律:

不要凭直觉优化。先测量,再动手;改完再测量,确认真的有效。

cntvct_el0 计时器、numastat、KSYS 的 diff-fopt-infoverify_opts.sh——12 讲里的所有工具都是为这一条服务的。

进阶与拓展

  • 分析工具分工devkit py-perf 的独特价值是 --native 一次看清 Python + C 混合调用栈,代价是仅 openEuler、Python 3.7–3.11、仅 PID 模式(本文已列);cProfile 是标准库自带的确定性插桩,函数级耗时准确但有可观开销,适合纯 Python 层;py-spy 采样式、免改代码免重启,适合随时 attach 线上进程。可组合:py-spy 常驻初筛,py-perf 出火焰图细看混合栈。
  • "底层已用 NEON"意味着什么:NumPy 在 aarch64 默认走 NEON,PyTorch 官方 aarch64 版本的 CPU 算子内核同样有 NEON 向量化路径——所以调用现成库的 Python 代码通常不需要你碰指令集;真正要动手写 NEON 的是自研 C/C++ 扩展,那回到第 4 讲的改写方法。
  • 延伸路径:想继续吃硬件红利,KAE 的底座是 UADK 用户态加速框架(第 11 讲),从 UADK 接口文档入手可绕过 OpenSSL 直接编程;想做系统级调优回归,DevKit 除 py-perf 外还有 tuner/advisor 子工具(本文安装的 devkit-tuner、NumPy 一节用到的 devkit advisor vec-check 即此类),配合第 6 讲的 KSYS 使用。

参考来源

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐