苹果推出自己的ARM平台已经有几年了,向ARM转向的力度显然非常坚决。这两年搞苹果ARM开发也积累了一点经验,特别是CMake相关的,记录在这里以供参考。

什么是universal binary?

其实非常简单,就是丫的可执行文件里塞俩内容,一份ARM的,一份Intel的,运行的时候系统自动挑匹配的那一份执行。

怎么编译出universal binary?

编译的时候给Clang喂进去两个架构参数-arch arm64 -arch x86_64,它就会编译出含两份二进制的对象文件,链接出来的东西就也会含两个平台。

对于CMake,只需要设置全局缓存变量CMAKE_OSX_ARCHETECTURESx86_64;arm64,它就会把这个传给编译器。

需要特别注意一个问题:架构是在编译器执行的时候才确定的,在此之前不能区分。所以如果使用了构建工具,你不能在编译阶段之前进行CPU平台特异的操作。比如对于CMake,你不能依据CMAKE_SYSTEM_PROCESSOR的值在CMake的configure阶段去区分使用不同CPU的源代码文件:

# 这样不可以,因为两个目标平台其实都会存在,在configure时不能区分
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64")
    target_sources(my_target PRIVATE arm_impl.cpp)
elif(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
    target_sources(my_target PRIVATE x86_impl.cpp)
endif()

而是需要把两个CPU平台的代码全都塞进去,让编译器都能看到,然后在代码里依据宏进行切换:

target_sources(my_target PRIVATE arm_impl.cpp x86_impl.cpp)
// x86_impl.cpp:
#if defined __x86_64__
// x86 implementation
#endif
// arm_impl.cpp
#if defined __arm__ || defined __arm64__
// arm implementation
#endif

我以前手搓的Intel SIMD intrinsics怎么办?

有个好人写了个库叫sse2neon,用ARM NEON实现了大部分的128位Intel SIMD的intrinsics。它是header-only的,大部分情况下,你可以简单地无伤替换:

#if defined __arm__ || defined __arm64__
#include <sse2neon.h>
#else
#include <什么mmintrin.h>
#endif

不过它有一些限制:

  • 它显然不能处理256位AVX,因为NEON就没有那么长。
  • 有些指令没实现,你可以手搓补完。
    说实话,个人认为NEON的设计比SSE要高明得多,指令正交性好,所以大部分时候用NEON冒充SSE并不困难。

Rosetta 有什么限制?

这玩意真挺牛逼的,在ARM平台上直接执行x86的代码,只有很少一部分性能损失。

按照我的经验,它只有两种情况有问题:

  1. 它只能翻译128位的SIMD指令,只要执行到256位的AVX就会立即挂illegal instruciton。这可以通过运行时判断CPU类型来解决。挂着Rosseta的程序在执行CPUID的时候,它会把自己汇报成只支持SSE4.2的CPU,所以你只需要严格地把公共代码限制在SSE4.2以下就行,然后涉及运算密集的代码搞成多份实现,只在支持的CPU上使用256位的AVX实现,不支持的CPU上只使用128位的。这样就能自然地支持Rosseta。
  2. 程序里有运行时生成代码的骚操作,比如VMProtect的virtualization和ultra模式。这会直接把Rosseta搞傻掉。

jiandingzhe
71 声望5 粉丝

引用和评论

0 条评论