1. 概要
基礎庫 libbase.a 基于 android ndk r18b 編譯, 被算法庫 libfoo.so 和算法庫 libbar.a 依賴, 算法庫則分別被 libapp1.so 和 libapp2.so 依賴。
libapp1.so 的開發者向 libfoo.so 的開發者反饋了鏈接報錯:
error: undefined symbol: __aarch64_ldadd4_acq_rel
libapp2.so 的開發者向 libbar.a 的開發者反饋了鏈接報錯:
undefined reference to `__aarch64_ldadd4_acq_rel'
libapp2.so 的開發者通過升級 NDK 版本解決了問題。 由于沒有 libbar.a 和 libapp2.so 對應的 ndk 詳細版本和最小復現代碼,
在搜索引擎上以 “undefined reference to `__aarch64_ldadd4_acq_rel’” 為關鍵字找到相似案例并分析, 確定了升級 NDK 能消除上述鏈接報錯的原因。
2. opencv issue 24856
2.1 報錯描述
https://github.com/opencv/opencv/issues/24856 (參考鏈接[1])
用戶 kevinzezel 在 android 平臺, 使用 OpenCV 4.9.0 + ncnn, 遇到鏈接報錯, 而使用 OpenCV 4.5.5~4.8.0 時沒有報錯。 報錯信息是:
undefined reference to `__aarch64_ldadd8_acq_rel’
opencv 維護人員 asmorkalov 提議讓用戶開啟 -mno-outline-atomics
編譯選項, 用戶 kevinzezel 反饋說 ndk 21.4.7075529 不支持這一選項.
catboost 維護者 andrey-khropov 說, 很可能是 clang 13.0.0-rc1 版本引入的問題, 對應到 commit: https://github.com/llvm/llvm-project/commit/c5e7e649d537067dec7111f3de1430d0fc8a4d11 (參考鏈接[2])。 解決方法: 要么使用低版本 clang (< 13.0.0), 要么用高版本 clang 但是傳入 -mno-outline-atomics
選項。
用戶 bvnp43 說, 升級 ndk 到 26.2.11394342 后問題解決。
用戶 chenyan-master 說, 編譯 V8 時遇到類似問題 (ndk 22.0.7026061), 升級 ndk 版本解決了。
2.2 分析
ncnn 版本沒有被提及, 大概率是沒有變化。 OpenCV 從 4.8.0 到 4.9.0 引發了鏈接報錯, 猜測是因為 OpenCV 官方編譯 android sdk 時更換了 ndk 版本, 而 ndk 版本和 clang 版本有一定的對應關系。
https://stackoverflow.com/questions/53385892/find-the-ndk-version-used-for-building-opencv-android-native-libraries (參考鏈接[3]) 給出了從 OpenCV 靜態庫反推編譯他它的 ndk 版本、 ndk 里的 clang 版本的方法:
strings libopencv_core.a | ag "Android NDK"
strings libopencv_core.a | ag "C++ Compiler"
opencv | ndk | clang |
---|---|---|
4.9.0 | 25.2.9519653 | 14.0.7 |
4.8.0 | 18.1.5063045 | 7.0.2 |
ndk 和 clang 的版本對應關系, 則通過 clang++ --version
確定, e.g.
D:/soft/android-ndk/r21e/toolchains/llvm/prebuilt/windows-x86_64/bin/clang++ --version
ndk 的數字形式的 x.y.z
版本, 和字母形式的版本如 r21e
, 則通過 <ndk-目錄>/source.properties
確定, 例如 ndk-r21e:
Pkg.Desc = Android NDK
Pkg.Revision = 21.4.7075529
整理后的 ndk 和 clang 版本對應關系如下:
ndk | ndk | clang |
---|---|---|
r28b | 28.1.13356709 | 19.0.0 |
r27c | 27.2.12479018 | 18.0.3 |
r26d | 26.3.11579264 | 17.0.2 |
r25c | 25.2.9519653 | 14.0.7 |
r25b | 25.1.8937393 | 14.0.6 |
r24 | 24.0.8215888 | 14.0.1 |
r23c | 23.2.8568313 | 12.0.9 |
r22b | 22.1.7171670 | 11.0.5 |
r21e | 21.4.7075529 | 9.0.9 |
r21b | 21.1.6352462 | 9.0.8 |
r20b | 20.1.5948944 | 8.0.7 |
r19c | 19.2.5345600 | 8.0.2 |
r18b | 18.1.5063045 | 7.0.2 |
2.3 結論
根據前一節的兩個表格知道:
opencv 4.8.0 是用 ndk-r18b 編譯, 對應的編譯器是 clang 7.0.2
opencv 4.9.0 是基于 ndk-r25c 編譯, 對應的編譯器是 clang 14.0.7
ndk 沒有使用過 clang 13.0.0 版本, 是從 clang 12.0.9 (ndk-r23c) 直接升級到 clang 14.0.1 (ndk-r24)
編譯選項 -moutline-atomics
和 -mno-outline-atomics
是在 clang 13.0.0 引入的, ndk-r21e 使用的是 clang 9.0.9, 因此會提示“不支持 -mno-outline-atomics 選項”.
3. libgcc.a 的類似報錯: __aarch64_swp4_acq_rel
3.1 問答描述
https://stackoverflow.com/questions/75045297/libgcc-linker-error-hidden-symbol-aarch64-swp1-acq-rel-in-libgcc-a-is-referen (參考鏈接[4]) 給出了 gcc aarch64 下的鏈接報錯, __aarch64_swp4_acq_rel
符號找不到, 這和 opencv issue 24856 報錯很像, 符號名字結尾略有差別。
用戶 Bill Cong 的回答給出了很好的最小復現: 定義和使用 std::atomic<int>
變量, 使用 -O1
優化等級, 在不同編譯器下查看對應的反匯編,
#include <atomic>std::atomic<int> ai(3);int main() {return ai.exchange(5);
}
-
ARM64 gcc 9.3, 不會生成
__aarch64_swp4_acq_rel
匯編指令 -
ARM64 gcc 10.2, 會生成
__aarch64_swp4_acq_rel
匯編指令 -
ARM64 gcc 10.2, 傳入
-mno-outline-atomics
選項, 不會生成__aarch64_swp4_acq_rel
匯編指令
https://godbolt.org/z/ssK3GGaoE (參考鏈接[5])
3.2 分析
-m-outline-atomics
編譯選項是哪個版本引入 GCC 的? 含義是什么?
https://stackoverflow.com/questions/65239845/how-to-enable-mno-outline-atomics-aarch64-flag (參考鏈接[6]) 提到, gcc 9.4 開始提供 -m-outline-atomics
編譯選項。
https://gcc.gnu.org/gcc-9/changes.html (參考鏈接[7]) 則證實了這一點
The option -moutline-atomics has been added to aid deployment of the Large System Extensions (LSE) on GNU/Linux systems built with a baseline architecture targeting Armv8-A. When the option is specified code is emitted to detect the presence of LSE instructions at run time and use them for standard atomic operations. For more information please refer to the documentation.
有人在編譯 Android FFmpeg 5.0 后的運行階段遇到報錯 (參考鏈接 [8])
dlopen failed: cannot locate symbol “__aarch64_ldadd8_acq_rel”
參考鏈接[9] 則給出了關于 LSE 的進一步解釋:
Out-of-line Atomics for LSE deploymentAArch64 Large System Extensions (LSE) were introduced in Armv8.1-A. These provide more efficient atomic instructions for large multi-core systems.LLVM 12 adds support for a new flag ‘-moutline-atomics', which detects at runtime whether the processor supports LSE. It then uses these new atomic instructions if possible, falling back to Armv8.0-A LL/SC loops on processors without LSE support. This option behaviour mirrors similar support available within GNU family of projects. We are working towards making this option enabled by default in the upcoming LLVM13 release.
即:
Armv8.1-A 引入了 LSE(大系統擴展), 用于在多核系統上提供更高效的原子指令。 LLVM12 添加了 -moutline-atomics
編譯選項, 在運行時檢查處理器是否支持 LSE, 如果存在則使用, 不存在則回退到 Arm8.0-A 的 LL/SC。 這個選項在 GNU 家族的工程中也提供了(就是 GCC)。 在 LLVM13 中 -moutline-atomics
選項則被默認開啟。
clang 12.0.1 開始,添加了 -moutline-atomics
和 -mno-outline-atomics
選項(參考鏈接 [10]):
而根據參考鏈接 [11], LLVM 的開發者們討論提到, gcc 9.3.1 開始提供了 LSE 的支持, 并在 gcc 10.1 默認開啟:
Outline atomics were added with gcc 9.3.1 and turned on by default in gcc 10.1
3.3 結論
-moutline-atomics
編譯選項表示開啟 LSE (Large System Extension), 意思是在運行時提供更高效率的原子操作, 在 Arm8.1-a 上起作用, 在 Arm8.0 上回退到原本的原子操作處理上。 -mno-outline-atomics
則關閉這一編譯選項。
-moutline-atomics
是在 gcc 9.3.1 版本開始提供, 在 clang 12.0.1 版本開始提供, 但都是默認不開啟狀態。 在 gcc 10.1 版本和 clang 13.0.0-rc1 版本中默認開啟了 -moutline-atomics
編譯選項。
4. __aarch64_ldadd4_acq_rel
和 __aarch64_swp4_acq_rel
指令, 對應的 C/C++ 代碼是什么?
4.1 C++11
基于參考鏈接[5], 很容易構造如下代碼, 并在 armv8-a clang 13.0.0 生成 __aarch64_ldadd4_acq_rel
指令 (仍然使用 -O1
):
https://godbolt.org/z/T7vzesfxd (參考鏈接[12])
#include <atomic>
std::atomic<int> ai(3);int main() {ai.fetch_add(1, std::memory_order_acquire);return ai.exchange(5);
}
匯編如下, 生成了 __aarch64_ldadd4_acq
和 __aarch64_swp4_acq_rel
指令.
main:stp x29, x30, [sp, #-32]!str x19, [sp, #16]mov x29, spadrp x19, aiadd x19, x19, :lo12:aimov w0, #1mov x1, x19bl __aarch64_ldadd4_acqmov w0, #5mov x1, x19bl __aarch64_swp4_acq_relldr x19, [sp, #16]ldp x29, x30, [sp], #32retai:.word 3
4.2 C++03: ncnn XADD 宏的實現
ncnn 庫使用 C++03 編譯, 在大部分平臺使用到了 __atomic_fetch_add
, __sync_fetch_add_add
等類似的 builtin 函數:
https://github.com/Tencent/ncnn/blob/20250503/src/allocator.h#L105-L151 (參考鏈接[13])
#if NCNN_THREADS
// exchange-add operation for atomic operations on reference counters
#if defined __riscv && !defined __riscv_atomic
// riscv target without A extension
static NCNN_FORCEINLINE int NCNN_XADD(int* addr, int delta)
{int tmp = *addr;*addr += delta;return tmp;
}
#elif defined __INTEL_COMPILER && !(defined WIN32 || defined _WIN32)
// atomic increment on the linux version of the Intel(tm) compiler
#define NCNN_XADD(addr, delta) (int)_InterlockedExchangeAdd(const_cast<void*>(reinterpret_cast<volatile void*>(addr)), delta)
#elif defined __GNUC__
#if defined __clang__ && __clang_major__ >= 3 && !defined __ANDROID__ && !defined __EMSCRIPTEN__ && !defined(__CUDACC__)
#ifdef __ATOMIC_ACQ_REL
#define NCNN_XADD(addr, delta) __c11_atomic_fetch_add((_Atomic(int)*)(addr), delta, __ATOMIC_ACQ_REL)
#else
#define NCNN_XADD(addr, delta) __atomic_fetch_add((_Atomic(int)*)(addr), delta, 4)
#endif
#else
#if defined __ATOMIC_ACQ_REL && !defined __clang__
// version for gcc >= 4.7
#define NCNN_XADD(addr, delta) (int)__atomic_fetch_add((unsigned*)(addr), (unsigned)(delta), __ATOMIC_ACQ_REL)
#else
#define NCNN_XADD(addr, delta) (int)__sync_fetch_and_add((unsigned*)(addr), (unsigned)(delta))
#endif
#endif
#elif defined _MSC_VER && !defined RC_INVOKED
#define NCNN_XADD(addr, delta) (int)_InterlockedExchangeAdd((long volatile*)addr, delta)
#else
但查看 ncnn 庫文件的反匯編, 例如 ncnn-20250503-android-shared.zip, 并沒有找到 __atomic_fetch_add
指令:
aarch64-linux-android-objdump -d ncnn-20250503-android-shared/arm64-v8a/lib/libncnn.so | ag '__aarch64_ldadd4_acq'
原因是 ncnn 的 CMakeLists.txt 里主動開啟了 -mno-outline-atomics
:
https://github.com/Tencent/ncnn/blob/master/src/CMakeLists.txt#L648-L652
if(ANDROID_NDK_MAJOR AND (ANDROID_NDK_MAJOR GREATER_EQUAL 23))# llvm 12 in ndk-23 enables out-of-line atomics by default# disable this feature for fixing linking atomic builtins issue with old ndktarget_compile_options(ncnn PRIVATE -mno-outline-atomics)endif()
(此處存疑, 可能是 ncnn 注釋 typo, 個人理解是 llvm 13 和 ndk-r25 默認開啟 out-of-line atomics; nihui: 使用 __aarch64_ldadd4_acq_rel后,樹莓派上會 crash,鏈接器會有undefined行為, 因此 ncnn 未開啟 )
4.3 自行構造
構造了簡單直白的一份 MY_XADD 宏的實現: https://godbolt.org/z/x567r5Gfr (參考鏈接[14])
#if _MSC_VER
# include <windows.h>
# define MY_XADD(addr, delta) ::InterlockedExchangeAdd((volatile long*)(addr), delta)
#elif __linux__
# define MY_XADD __sync_fetch_and_add
#endifint main() {int refcount = 1;return MY_XADD(&refcount, -1);
}
使用 -O1 優化等級, 在各個編譯器下結果如下:
- armv8-a clang 12.0.0: 生成 “平凡的” ldaxr, stlxr 指令
- armv8-a clang 13.0.0: 生成
__aarch64_ldadd4_acq_rel
指令 - armv8-a clang 13.0.0, 生成
-mno-outline-atomics
編譯選項: 生成 “平凡的” ldaxr, stlxr 指令 - arm64 msvc v19.43 VS17.13: 生成 “平凡的” ldaxr, stlxr 指令
- x86-64 gcc 15.1: 生成
lock xadd
指令
ldaxr
指令的解釋:
Load-Acquire Exclusive Register derives an address from a base register value, loads a 32-bit word or 64-bit doubleword from memory, and writes it to a register. The memory access is atomic. The PE marks the physical address being accessed as an exclusive access. This exclusive access mark is checked by Store Exclusive instructions. See Synchronization and semaphores. The instruction also has memory ordering semantics as described in Load-Acquire, Store-Release. For information about memory accesses, see Load/Store addressing modes.
Load-Acquire Exclusive Register 指令根據基址寄存器的值計算地址,從內存中讀取一個 32 位字(word)或 64 位雙字(doubleword),并寫入目標寄存器。此內存訪問是原子的。處理單元(PE)會將正在訪問的物理地址標記為“獨占訪問”,該標記稍后由 Store Exclusive 指令檢查。詳見“同步與信號量”一節。
此外,該指令具有 Load-Acquire/Store-Release 描述的內存排序語義。有關內存訪問的更多信息,請參閱“裝載/存儲尋址模式”。
stlxr
指令的解釋:
Store-Release Exclusive Register stores a 32-bit word or a 64-bit doubleword to memory if the PE has exclusive access to the memory address, from two registers, and returns a status value of 0 if the store was successful, or of 1 if no store was performed. See Synchronization and semaphores. The memory access is atomic. The instruction also has memory ordering semantics as described in Load-Acquire, Store-Release. For information about memory accesses, see Load/Store addressing modes.
Store-Release Exclusive Register(存儲-釋放獨占寄存器)指令在處理元(PE)對某內存地址擁有獨占訪問權限時,可將兩個寄存器中的數據寫入內存:寫入 32 位字或 64 位雙字。如果寫入成功,指令返回狀態值 0;若未執行寫入,則返回狀態值 1。參見“同步與信號量”。
該內存訪問是原子的。此指令還具備與“Load-Acquire / Store-Release”描述一致的內存排序語義。關于內存訪問的詳細說明,請參閱“加載/存儲尋址模式”。
4.4 分析 libbase.a 庫
前一節構造的最小復現代碼,MY_XADD
宏的定義和使用, 對應到文章開頭 libbase.a
基礎庫中的 BASE_XADD
宏實現:
base/atomic.hpp
#if _MSC_VER
# include <windows.h>
# define BASE_XADD(addr, delta) ::InterlockedExchangeAdd((volatile long*)(addr), delta)
#elif __linux__
# define BASE_XADD __sync_fetch_and_add
#endif
base/atomic.hpp
被不同的編譯器處理:
-
ndk-r18: clang 7.0.2, 編譯出 libbase.a
-
ndk-r25c 或更高版本: clang >= 14.0.7, 編譯出 libfoo.so 和 libbar.a
猜測 libapp1.so
和 libapp2.so
不會接觸到 base/atomic.hpp
, 并且這兩個 app so 是用 ndk-r24 或更低版本編譯。 這樣一來, libfoo.so 和 libbar.a 的編譯過程產生了非預期的 __aarch64_ldadd4_acq_rel
匯編指令, 下游的集成人員遇到鏈接報錯, 集成人員換到 clang >= 13.0.0 的 ndk 版本, 也就是 ndk >= r25, 就避免了報錯。
另一種改法: 是 libfoo.so
和 libbar.a
的構建過程, 做類似于 ncnn 的配置, 判斷 ndk >= r24 并添加 -mno-outline-atomics
編譯選項。
5. 總結
gcc 9.3.1 和 clang 12.0.1 添加了 -moutline-atomics
編譯選項, 用來開啟 LSE (Large System Extension), 用于在 Armv8.1-a 上改善多核情況下的原子操作的性能。
這個選項在 gcc 9 和 clang 12 是默認不開啟的, 相當于是默認開啟了 -mno-outline-atomics
。
從 gcc 10 和 clang 13 開始默認開啟 -moutline-atomics
編譯選項。
ndk 版本和 clang 版本有對應的關系, clang >= 13 對應到 ndk >= r24。
當使用 clang >= 13 (ndk >= r24) 編譯了帶有 __sync_fetch_and_add
(C/C++ Builtin) 或 C++11 的 std::atomic<T>
的代碼, 例如 opencv/ncnn 中的 XADD
的實現, 會生成 __aarch64_ldadd4_acq_rel
等 LSE 相關的匯編指令, 這樣的二進制被 clang < 13 (ndk < r24) 的編譯器做鏈接, 會遇到 __aarch64_ldadd4_acq_rel
符號找不到的問題。
當遇到上述報錯, 可以對齊編譯器版本到 clang >= 13 (ndk >= r24), 或手動指定 -mno-outline-atomics
選項。
如果是 clang 12 或 gcc 9 編譯器, 但傳入了 -moutline-atomics
選項, 也會發生類似的鏈接報錯, 處理方式同前。
當然, 考慮到 ndk-r24 沒有 ndk-24b, ndk-r24c 等版本, 建議用 ndk >= r25c 的版本。
References
- [1] https://github.com/opencv/opencv/issues/24856
- [2] https://github.com/llvm/llvm-project/commit/c5e7e649d537067dec7111f3de1430d0fc8a4d11
- [3] https://stackoverflow.com/questions/53385892/find-the-ndk-version-used-for-building-opencv-android-native-libraries
- [4] https://stackoverflow.com/questions/75045297/libgcc-linker-error-hidden-symbol-aarch64-swp1-acq-rel-in-libgcc-a-is-referen
- [5] https://godbolt.org/z/ssK3GGaoE
- [6] https://stackoverflow.com/questions/65239845/how-to-enable-mno-outline-atomics-aarch64-flag
- [7] https://gcc.gnu.org/gcc-9/changes.html
- [8] https://juejin.cn/post/7149468268674154510
- [9] https://community.arm.com/arm-community-blogs/b/tools-software-ides-blog/posts/llvm12-for-arm
- [10] https://releases.llvm.org/12.0.1/tools/clang/docs/ReleaseNotes.html
- [11] https://reviews.llvm.org/D93585
- [12] https://godbolt.org/z/T7vzesfxd
- [13] https://github.com/Tencent/ncnn/blob/20250503/src/allocator.h#L105-L151
- [14] https://godbolt.org/z/x567r5Gfr