ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

【Bug已解决】C API memory leak Asan 解决方案

【Bug已解决】C API memory leak Asan 解决方案

【Bug已解决】C API memory leak Asan 解决方案

一、现象长什么样

用 ONNX Runtime 的 C API(不是 Python/Java 高层封装)写程序,并用 AddressSanitizer (ASAN) 编译运行,ASAN 在退出时报告内存泄漏,指向 C API 分配的某块内存没有被释放:

==12345==ERROR: LeakSanitizer: detected memory leak Direct leak of 1024 byte(s) in 1 object(s) allocated from: #0 ort::... Allocator::Alloc #1 OrtGetApiBase / Ort::...::... (C API 内部)

最小触发(C API):

#include <onnxruntime/core/session/onnxruntime_c_api.h> int main() { OrtEnv* env = NULL; OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, "demo", &env); // ... 用 C API 创建 session、跑推理 ... OrtReleaseEnv(env); // 必须配对的 Release 调用 return 0; } // ASAN 报泄漏:某块 C API 内部分配没被对应 Release 释放

注意:程序能跑出正确结果,只是 ASAN 抓到泄漏。这是C API 的资源释放契约问题——分配的每块内存都要有对应的OrtReleaseXxx调用,漏一个就泄漏。

二、背景

ONNX Runtime 的 C API 遵循“谁分配、谁释放、用显式 Release 函数”的契约。C API 里几乎所有不透明句柄(OrtEnvOrtSessionOrtValueOrtRunOptionsOrtMemoryInfoOrtAllocator等)都是内部在堆上分配的,调用方拿到指针后,必须在不用时调用对应的OrtReleaseXxx归还。

ASAN 会在程序退出时扫描所有未释放的堆块,报告泄漏。常见的泄漏来源:

  • 忘记配对 Release:创建了OrtValue/OrtRunOptions/OrtMemoryInfo却没OrtReleaseValue/OrtReleaseRunOptions/OrtReleaseMemoryInfo
  • API 本身漏释放:某些 C API 函数在内部Alloc了一块(比如返回字符串、返回数组),但没有提供对应的 Release 函数,或文档没说要释放,调用方无从释放 -> 泄漏在 API 侧。
  • 异常路径提前返回:C++ 封装(C++ API)用了 RAII,但裸 C 调用里中间出错goto/提前 return,跳过了后面的 Release。
  • 长期持有不释放:循环里反复创建OrtValue但不释放,累积泄漏。

三、根因

根因是C API 分配的某些资源没有对应的 Release 调用被发起(要么调用方漏写,要么 API 没提供 Release),ASAN 退出时抓到未释放堆块:

  1. 调用方漏配对 Release:最常见的泄漏——OrtCreateXxx了但忘了OrtReleaseXxx,或错误/提前返回路径跳过了 Release。
  2. API 侧缺 Release 入口:某些函数返回的堆内存(如OrtGetErrorMessage返回的字符串、Ort::...某些数组)需要特定 Release 释放,但 API 没暴露或文档不清,调用方释放不了 -> 泄漏在 API 实现里。
  3. C++ 封装与裸 C 混用:用了 C++ API 的 RAII 对象,但中间又手动new/取了裸指针没管。
  4. 不是推理错:结果正确,只是资源释放契约被破坏,ASAN 抓泄漏。

所以这不是数值错,而是C API 资源释放契约没被完整履行(漏 Release 或 API 不给 Release)

四、最小可运行复现

下面用 C 标准库模拟“Alloc 了但没 Release -> ASAN 报泄漏”的精简模型:

#include <stdlib.h> #include <stdio.h> // 模拟 ORT C API:Alloc 必须配对 Release void* OrtAlloc(size_t n) { return malloc(n); } void OrtRelease(void* p) { free(p); } int main() { void* a = OrtAlloc(1024); // 分配 // 忘记 OrtRelease(a); // <-- 漏了配对 Release -> 泄漏 (void)a; return 0; } // 用 clang -fsanitize=address 编译运行 -> LeakSanitizer 报 1024 字节泄漏

clang -fsanitize=address -g demo.c -o demo && ./demo会报Direct leak of 1024 byte(s),正是“Alloc 不 Release”的精简复现。把OrtRelease(a)加回去泄漏就消失。

五、解决方案(第一层:最小直接修复)

最小修复:给每个OrtCreateXxx/OrtAlloc配对的OrtReleaseXxx,覆盖所有返回路径(包括错误/提前返回)。推荐用 C++ RAII 封装避免手漏:

#include <onnxruntime/core/session/onnxruntime_cxx_api.h> int main() { Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "demo"); // RAII,析构自动 Release Ort::SessionOptions so; Ort::Session session(env, "model.onnx", so); // RAII // 用 C++ API 的 Ort::Value(RAII)管理输入输出,析构自动释放 Ort::MemoryInfo mi("Cpu", OrtDeviceAllocator, 0, OrtMemTypeDefault); // 所有 Ort:: 对象离开作用域自动 Release,杜绝漏配 return 0; }

如果必须用裸 C API,用goto cleanup/defer模式确保每条路径都 Release:

OrtValue* val = NULL; OrtStatus* st = OrtCreateValue(..., &val); if (st) { /* 处理错误并 goto cleanup */ } // ... 使用 val ... cleanup: if (val) OrtReleaseValue(val); // 所有路径都释放 if (st) OrtReleaseStatus(st);

这一层立刻消除大多数调用方泄漏。

六、解决方案(第二层:结构性改进)

把“每个 C API 句柄的配对 Release 契约”收口成唯一的配置对象OrtCApiAsanLeakPolicy,代码评审与静态检查读它:

from dataclasses import dataclass, field from typing import Tuple, Dict @dataclass(frozen=True) class OrtCApiAsanLeakPolicy: """C API 资源释放契约的单一事实来源。""" # 句柄 -> 必须配对的 Release 函数(全量映射) release_map: Tuple[Tuple[str, str], ...] = ( ("OrtEnv", "OrtReleaseEnv"), ("OrtSession", "OrtReleaseSession"), ("OrtValue", "OrtReleaseValue"), ("OrtRunOptions", "OrtReleaseRunOptions"), ("OrtMemoryInfo", "OrtReleaseMemoryInfo"), ("OrtAllocator", "OrtReleaseAllocator"), ("OrtStatus", "OrtReleaseStatus"), ) # 是否要求优先用 C++ RAII 封装(避免手漏) prefer_raii: bool = True # ASAN 构建下必须零泄漏 require_zero_leak_under_asan: bool = True def paired_release(self, handle: str) -> str: m = dict(self.release_map) return m.get(handle, "UNKNOWN") def describe(self) -> str: return "每个 OrtCreateXxx 必须配对 OrtReleaseXxx,优先 RAII,ASAN 零泄漏" POLICY = OrtCApiAsanLeakPolicy() def check_release(handle: str, policy: OrtCApiAsanLeakPolicy = POLICY) -> str: return policy.paired_release(handle)

所有 C API 使用与评审读同一份POLICY,释放契约被固化,漏配一眼可见。

七、解决方案(第三层:断言 / CI 守护)

把“C API 无泄漏(ASAN 通过)”做成断言。下面用 pytest 风格守护(用模拟对象验证配对):

import pytest def test_every_handle_has_release(policy): for handle, rel in policy.release_map: assert rel.startswith("OrtRelease") assert rel != "UNKNOWN" def test_prefer_raii(policy): assert policy.prefer_raii is True def test_asan_zero_leak_required(policy): assert policy.require_zero_leak_under_asan is True def test_release_pairing_complete(): handles = ["OrtEnv", "OrtSession", "OrtValue", "OrtRunOptions"] for h in handles: assert check_release(h) == f"OrtRelease{h[3:]}" # OrtEnv->OrtReleaseEnv

这四组断言锁住:(1) 每个句柄都有 Release;(2) 优先 RAII;(3) ASAN 零泄漏要求;(4) 配对完整。CI 跑通即代表释放契约被守护。

八、排查清单

遇到 C API 程序 ASAN 报泄漏:

  1. 看泄漏栈指向哪个 Alloc:定位是哪个 Ort 句柄没释放。
  2. 查配对 Release:有没有OrtReleaseXxx对应每个OrtCreateXxx/OrtAlloc
  3. 查所有返回路径:错误/提前 return 路径是不是也 Release 了。
  4. 优先改 C++ RAII:用Ort::封装,析构自动释放,避免手漏。
  5. API 侧泄漏:若泄漏在 ORT 内部(没提供 Release),向 ORT 提 issue 补 Release。
  6. 统一策略对象:用OrtCApiAsanLeakPolicy固化释放映射。
  7. CI 守护:ASAN 构建必须零泄漏,断言配对完整。

九、小结

C API memory leak Asan的根因是:ONNX Runtime 的 C API 遵循“分配必须配对 Release”的契约,但使用方漏写OrtReleaseXxx(尤其错误/提前返回路径),或某些 API 内部分配的内存没有对应的 Release 入口,ASAN 在退出时抓到未释放堆块。

最小修复是给每个OrtCreateXxx配对的OrtReleaseXxx、覆盖所有返回路径,优先用 C++ RAII 封装(Ort::)自动释放;结构性改进是用唯一的OrtCApiAsanLeakPolicy固化句柄-Release 映射;CI 用四组断言守护“每句柄有 Release、优先 RAII、ASAN 零泄漏、配对完整”。记住:C API 里每个 Alloc 都要有 Release,缺一个 ASAN 就报泄漏,RAII 是最省心的防线。

返回列表