1 思路

由于 ARMv7 开始支持 Neon 指令集, 因此可以把 SSE 指令翻译成 Neon 指令. 有一些开源项目已经支持这种”指令翻译”, 比如 sse2neon, simde 等. 目前来看 simde 支持的最好.

当然这种思路也有限制, 只支持小端字节序的平台. 如果要移植到大端字节序平台, 则需要改动 hyperscan 大量源码… 几乎是不可能的任务, 太难维护了.

我移植的代码见 github.com/zzqcn/hyperscan/ 中的 porting 分支.

2 编译依赖项

需要先安装:

  • 交叉编译工具链(toolchain) 本文假设放在 /opt/toolchain
  • cmake
  • ragel
  • Boost库 下载压缩包后解压即可, 不需要编译
  • SIMDe 本文假设安装在 /dev/zzq/dev/simde

    3 配置cmake编译环境

    准备toolchain file

    在 cmake 下面建立 toolchain 子目录, 编写 armv7.cmake 文件: ```makefile

    ARMv7

    set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm)

set(TOOLCHAIN_DIR /opt/toolchain/crosstools-arm-gcc-5.3-linux-4.1-glibc-2.22-binutils-2.25) set(TOOLCHAIN_INCLUDE ${TOOLCHAIN_DIR}/usr/include ${TOOLCHAIN_DIR}/usr/arm-buildroot-linux-gnueabi/sysroot/include ) set(TOOLCHAIN_LIB ${TOOLCHAIN_DIR}/usr/lib )

set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/usr/arm-buildroot-linux-gnueabi/sysroot)

set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/usr/bin/arm-buildroot-linux-gnueabi-gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/usr/bin/arm-buildroot-linux-gnueabi-g++)

set(CMAKE_C_COMPILER_WORKS 1) set(CMAKE_CXX_COMPILER_WORKS 1)

set(CMAKE_C_FLAGS “${CMAKE_C_FLAGS} -march=armv7-a -mfpu=neon”)

set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -march=armv7-a -mfpu=neon”)

set(CMAKE_FIND_ROOT_PATH ${TOOLCHAIN_DIR}/usr/arm-buildroot-linux-gnueabi)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

set(SIMDE_INCLUDE /home/zzq/dev/simde/simde CACHE PATH “SIMDe include directory”)

include_directories( ${TOOLCHAIN_DIR}/usr/arm-buildroot-linux-gnueabi/sysroot/include ${TOOLCHAIN_DIR}/usr/include ${SIMDE_INCLUDE} )

  1. 具体细节请参考 [https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html](https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html)
  2. <a name="hZ5i5"></a>
  3. ## 修改CMakeList.txt
  4. 屏蔽掉编译时会出错的行.<br />一处是会检查 `-march` , `-mtune` , 把这里设置为在x86平台编译时才检查:
  5. ```makefile
  6. if (CMAKE_SYSTEM_PROCESSOR MATCHES "^x86")
  7. # arg1 might exist if using ccache
  8. string (STRIP "${CMAKE_C_COMPILER_ARG1}" CC_ARG1)
  9. set (EXEC_ARGS ${CC_ARG1} -c -Q --help=target -march=native -mtune=native)
  10. execute_process(COMMAND ${CMAKE_C_COMPILER} ${EXEC_ARGS}
  11. OUTPUT_VARIABLE _GCC_OUTPUT)
  12. string(REGEX REPLACE ".*march=[ \t]*([^ \n]*)[ \n].*" "\\1"
  13. GNUCC_ARCH "${_GCC_OUTPUT}")
  14. # test the parsed flag
  15. set (EXEC_ARGS ${CC_ARG1} -E - -mtune=${GNUCC_ARCH})
  16. execute_process(COMMAND ${CMAKE_C_COMPILER} ${EXEC_ARGS}
  17. OUTPUT_QUIET ERROR_QUIET
  18. INPUT_FILE /dev/null
  19. RESULT_VARIABLE GNUCC_TUNE_TEST)
  20. if (NOT GNUCC_TUNE_TEST EQUAL 0)
  21. message(SEND_ERROR "Something went wrong determining gcc tune: -mtune=${GNUCC_ARCH} not valid")
  22. endif()
  23. endif()

if (CMAKE_SYSTEM_PROCESSOR MATCHES "^x86") 是判断当前平台是不是 x86 的一个技巧, 下文会经常用到.

第二处是检查 x86 intrinsics 头文件, 这里也改为只在 x86 上检查:

  1. if (CMAKE_SYSTEM_PROCESSOR MATCHES "^x86")
  2. CHECK_INCLUDE_FILES(intrin.h HAVE_C_INTRIN_H)
  3. CHECK_INCLUDE_FILE_CXX(intrin.h HAVE_CXX_INTRIN_H)
  4. CHECK_INCLUDE_FILES(x86intrin.h HAVE_C_X86INTRIN_H)
  5. CHECK_INCLUDE_FILE_CXX(x86intrin.h HAVE_CXX_X86INTRIN_H)
  6. endif()

修改cmake/arch.cmake

这个文件会检查 x86 指令集可用性, 同样设置为只在 x86 上检查, 而在 ARM 平台假装支持 SSSE3.

  1. if (CMAKE_SYSTEM_PROCESSOR MATCHES "^x86")
  2. if (HAVE_C_X86INTRIN_H)
  3. set (INTRIN_INC_H "x86intrin.h")
  4. ...
  5. elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^arm")
  6. set(HAVE_SSSE3 TRUE)
  7. else()
  8. message(FATAL_ERROR "don't support processor " ${CMAKE_SYSTEM_PROCESSOR})
  9. endif()

修改cmake/platform.cmake

这个文件会检查当前平台, 改为在 x86 上检查, 而在 ARM 默认设置为 ARM32 位, 这里的设置最终会影响 config.h 的输出.

  1. # determine the target arch
  2. if (CMAKE_SYSTEM_PROCESSOR MATCHES "^x86")
  3. # really only interested in the preprocessor here
  4. CHECK_C_SOURCE_COMPILES("#if !(defined(__x86_64__) || defined(_M_X64))\n#error not 64bit\n#endif\nint main(void) { return 0; }" ARCH_64_BIT)
  5. CHECK_C_SOURCE_COMPILES("#if !(defined(__i386__) || defined(_M_IX86))\n#error not 64bit\n#endif\nint main(void) { return 0; }" ARCH_32_BIT)
  6. set(ARCH_X86_64 ${ARCH_64_BIT})
  7. set(ARCH_IA32 ${ARCH_32_BIT})
  8. elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^arm")
  9. # TODO: XXX
  10. set(ARCH_32_BIT TRUE)
  11. set(ARCH_ARM32 TRUE)
  12. else()
  13. message(FATAL_ERROR "don't support processor " ${CMAKE_SYSTEM_PROCESSOR})
  14. endif()

修改config.h.in

添加

  1. #cmakedefine ARCH_ARM32

这个文件相当于一个模板, 会根据 cmake 文件的判定结果生成最终的 config.h. 对于我们的 ARMv7 移植, config.h 中会生成 #define ARCH_ARM32 .

其他cmake小技巧

进行正则匹配, 检查当前平台名称

  1. if (CMAKE_SYSTEM_PROCESSOR MATCHES "^x86")
  2. elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^arm")
  3. set(HAVE_SSSE3 TRUE)
  4. else()

输出状态信息或错误信息:

  1. message(STATUS "XXXXXXX")
  2. message(FATAL_ERROR "XXXXXXXXXXX")

在 cmake 文件里控制 config.h.in 文件里的宏生成:

  1. # cmake:
  2. set(ARCH_32_BIT TRUE)
  3. set(ARCH_ARM32 TRUE)
  4. # config.h.in:
  5. #cmakedefine ARCH_32_BIT
  6. #cmakedefine ARCH_ARM32

打印 include 路径

  1. get_property(dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
  2. foreach(dir ${dirs})
  3. message(STATUS "dir='${dir}'")
  4. endforeach()

4 修改hyperscan源码

主要是修改 src/util 下面的代码.

修改arch.h

添加:

  1. #if defined(__arm__)
  2. #define NO_ASM
  3. #endif

修改intrinsics.h

把原有代码包含在 x86 的判断条件里, 而 arm 时包含 sse_helper.h.

  1. #if defined(ARCH_IA32) || defined(ARCH_X86_64)
  2. #ifdef __cplusplus
  3. # if defined(HAVE_CXX_X86INTRIN_H)
  4. # define USE_X86INTRIN_H
  5. ...
  6. #elif defined(ARCH_ARM32)
  7. #include "sse_helper.h"
  8. #endif

这个 sse_helper.h 是我们新添加的代码, 是移植的关键!

添加sse_helper.h

很简单, 把 hyperscan 用到的 SSSE 和之前的指令翻译成 SIMDe 的函数:

  1. #ifndef SSE_HELPER_H
  2. #define SSE_HELPER_H
  3. #include "x86/ssse3.h"
  4. #ifdef __cplusplus
  5. extern "C" {
  6. #endif
  7. #define _mm_cmpeq_epi8 simde_mm_cmpeq_epi8
  8. #define _mm_setzero_si128 simde_mm_setzero_si128
  9. #define _mm_xor_si128 simde_mm_xor_si128
  10. #define _mm_movemask_epi8 simde_mm_movemask_epi8
  11. #define _mm_cmpeq_epi32 simde_mm_cmpeq_epi32
  12. #define _mm_movemask_ps simde_mm_movemask_ps
  13. #define _mm_cmpeq_epi64 simde_mm_cmpeq_epi64
  14. #define _mm_slli_epi64 simde_mm_slli_epi64
  15. #define _mm_cvtsi32_si128 simde_mm_cvtsi32_si128
  16. #define _mm_srli_epi64 simde_mm_srli_epi64
  17. #define _mm_set1_epi8 simde_mm_set1_epi8
  18. #define _mm_set1_epi32 simde_mm_set1_epi32
  19. #define _mm_cvtsi128_si32 simde_mm_cvtsi128_si32
  20. #define _mm_set_epi64x simde_mm_set_epi64x
  21. #define _mm_srli_si128 simde_mm_srli_si128
  22. #define _mm_slli_si128 simde_mm_slli_si128
  23. #define _mm_extract_epi32 simde_mm_extract_epi32
  24. #define _mm_extract_epi64 simde_mm_extract_epi64
  25. #define _mm_and_si128 simde_mm_and_si128
  26. #define _mm_xor_si128 simde_mm_xor_si128
  27. #define _mm_or_si128 simde_mm_or_si128
  28. #define _mm_andnot_si128 simde_mm_andnot_si128
  29. #define _mm_load_si128 simde_mm_load_si128
  30. #define _mm_loadu_si128 simde_mm_loadu_si128
  31. #define _mm_storeu_si128 simde_mm_storeu_si128
  32. #define _mm_max_epu8 simde_mm_max_epu8
  33. #define _mm_min_epu8 simde_mm_min_epu8
  34. #define _mm_adds_epu8 simde_mm_adds_epu8
  35. #define _mm_sub_epi8 simde_mm_sub_epi8
  36. #define _mm_packs_epi16 simde_mm_packs_epi16
  37. #define _mm_packs_epi32 simde_mm_packs_epi32
  38. #define _mm_castsi128_ps simde_mm_castsi128_ps
  39. #define _mm_sll_epi64 simde_mm_sll_epi64
  40. #define _mm_shuffle_epi8 simde_mm_shuffle_epi8
  41. #define _mm_alignr_epi8 simde_mm_alignr_epi8
  42. #define _mm_set1_epi64x simde_mm_set1_epi64x
  43. #define _mm_set_epi32 simde_mm_set_epi32
  44. #ifdef __cplusplus
  45. }
  46. #endif
  47. #endif

修改simd_types.h

对于 ARM, 定义新的 m128i 类型.

  1. #if defined(ARCH_IA32) || defined(ARCH_X86_64)
  2. #if defined(HAVE_SSE2)
  3. typedef __m128i m128;
  4. #else
  5. typedef struct ALIGN_DIRECTIVE {u64a hi; u64a lo;} m128;
  6. #endif
  7. #if defined(HAVE_AVX2)
  8. typedef __m256i m256;
  9. #else
  10. typedef struct ALIGN_AVX_DIRECTIVE {m128 lo; m128 hi;} m256;
  11. #endif
  12. typedef struct {m128 lo; m128 mid; m128 hi;} m384;
  13. #if defined(HAVE_AVX512)
  14. typedef __m512i m512;
  15. #else
  16. typedef struct ALIGN_ATTR(64) {m256 lo; m256 hi;} m512;
  17. #endif
  18. #elif defined(ARCH_ARM32)
  19. typedef simde__m128i m128;
  20. typedef struct ALIGN_AVX_DIRECTIVE {m128 lo; m128 hi;} m256;
  21. typedef struct {m128 lo; m128 mid; m128 hi;} m384;
  22. typedef struct ALIGN_ATTR(64) {m256 lo; m256 hi;} m512;
  23. #endif

修改simd_utils.h

在 x86 上检查 SIMD 指令的最低依赖 - SSSE3, 而在 ARM 编译时默认支持.

  1. #if defined(ARCH_IA32) || defined(ARCH_X86_64)
  2. #if !defined(_WIN32) && !defined(__SSSE3__)
  3. #error SSSE3 instructions must be enabled
  4. #endif
  5. #endif

修改cpuid_flags.h

对 cpuid.h 的检查只在 x86 进行

  1. #if defined(ARCH_IA32) || defined(ARCH_X86_64)
  2. #if !defined(_WIN32) && !defined(CPUID_H_)
  3. #include <cpuid.h>
  4. /* system header doesn't have a header guard */
  5. #define CPUID_H_
  6. #endif
  7. #endif

修改cpuid_inline.h

这里的函数用来检查 cpu 特性支持, 对于 ARM 我们全返回 0 就完事了.

  1. #if defined(ARCH_IA32) || defined(ARCH_X86_64)
  2. #if !defined(_WIN32) && !defined(CPUID_H_)
  3. #include <cpuid.h>
  4. /* system header doesn't have a header guard */
  5. #define CPUID_H_
  6. #endif
  7. #endif
  8. #ifdef __cplusplus
  9. extern "C"
  10. {
  11. #endif
  12. #if defined(ARCH_IA32) || defined(ARCH_X86_64)
  13. static inline
  14. void cpuid(unsigned int op, unsigned int leaf, unsigned int *eax,
  15. unsigned int *ebx, unsigned int *ecx, unsigned int *edx) {
  16. ...
  17. #elif defined(ARCH_ARM32)
  18. static inline
  19. int check_ssse3(void) {
  20. return 1;
  21. }
  22. static inline
  23. int check_sse42(void) {
  24. return 0;
  25. }
  26. static inline
  27. int check_popcnt(void) {
  28. return 0;
  29. }
  30. static inline
  31. int check_avx2(void) {
  32. return 0;
  33. }
  34. static inline
  35. int check_avx512(void) {
  36. return 0;
  37. }
  38. #endif

修改cpuid_flags.c

让原有代码只在 x86 编译时编译, 在 arm 上 cpuid_flags 返回 0, cpuid_tune 返回 HS_TUNE_FAMILY_GENERIC .

  1. #if defined(ARCH_IA32) || defined(ARCH_X86_64)
  2. #if !defined(_WIN32) && !defined(CPUID_H_)
  3. #include <cpuid.h>
  4. #endif
  5. ...
  6. #elif defined(ARCH_ARM32)
  7. u64a cpuid_flags(void) {
  8. return 0;
  9. }
  10. u32 cpuid_tune(void) {
  11. return HS_TUNE_FAMILY_GENERIC;
  12. }
  13. #endif

5 编译

设置LD_LIBRARY_PATH环境变量

  1. $export LD_LIBRARY_PATH=/opt/toolchain/crosstools-arm-gcc-5.3-linux-4.1-glibc-2.22-binutils-2.25/usr/lib

设置SYSROOT环境变量

接下来就可以编译了. 编译步骤主要分以下几步

  1. 在 hyperscan 源代码目录以外的地方建立编译目录, 如 hs_build/
  2. 进入 hs_build, 运行 cmake, 检查编译环境并生成 Makefile
  3. 运行 cmake --build . 进行编译

下面说的都是第 2 步.

默认编译为静态库

  1. $cmake -DCMAKE_TOOLCHAIN_FILE=../hyperscan/cmake/toolchain/armv7.cmake -DCMAKE_C_FLAGS="-march=armv7-a -mfpu=neon" -DCMAKE_CXX_FLAGS="-march=armv7-a -mfpu=neon" -DBOOST_ROOT=/home/zzq/pkg/boost_1_71_0 ../hyperscan

默认的编译选项是带调试信息的 Release 版本( CMAKE_BUILD_TYPE=RelWithDebInfo ). 静态编译出来的加文件是非常大的:

  1. $du -sh lib/*
  2. 4.5M lib/libcorpusomatic.a
  3. 132K lib/libcrosscompileutil.a
  4. 72K lib/libdatabaseutil.a
  5. 428K lib/libexpressionutil.a
  6. 243M lib/libhs.a
  7. 18M lib/libhs_runtime.a

编译为动态库

  1. $cmake -DCMAKE_TOOLCHAIN_FILE=../hyperscan/cmake/toolchain/armv7.cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=on -DCMAKE_C_FLAGS="-march=armv7-a -mfpu=neon" -DCMAKE_CXX_FLAGS="-march=armv7-a -mfpu=neon" -DBOOST_ROOT=/home/zzq/pkg/boost_1_71_0 ../hyperscan

编译时的小技巧

显示当前平台名称或宏定义
gcc -dM -E - < /dev/null | grep ARCH

  1. # ARM
  2. $/opt/toolchain/crosstools-arm-gcc-5.3-linux-4.1-glibc-2.22-binutils-2.25/usr/bin/arm-buildroot-linux-gnueabi-gcc -dM -E - < /dev/null | grep ARCH
  3. #define __ARM_ARCH_ISA_ARM 1
  4. #define __ARM_ARCH_PROFILE 65
  5. #define __ARM_ARCH_ISA_THUMB 2
  6. #define __ARM_ARCH 7
  7. #define __ARM_ARCH_7A__ 1
  8. # MIPS
  9. $/opt/toolchain/mips-linux-uclibc-4.9.3/usr/bin/mips-buildroot-linux-uclibc-gcc -dM -E - < /dev/null | grep ARCH
  10. #define _MIPS_ARCH "mips32r2"
  11. #define _MIPS_ARCH_MIPS32R2 1

gcc arm编译选项: https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html
gcc mips编译选项: https://gcc.gnu.org/onlinedocs/gcc/MIPS-Options.html

编译时某些编译器可能会有找不到 math 库里 isnan 的问题, 见https://stackoverflow.com/questions/39130040/cmath-hides-isnan-in-math-h-in-c14-c11/39132787

6 单元测试

hyperscan 自带单元测试(unit/), 默认编译完成后, 会在编译目录的 bin 子目录生成 unit-hyperscan 可执行程序, 可执行它进行单元测试. 运行时间会很长, 在 i5-9400 主机的 Ubuntu14.04 虚拟机上会运行 1 个小时, 才会跑完所有测试用例.

  1. zzq@ubuntu14:~/dev/hs_build/bin
  2. $ls
  3. hscheck* simplegrep* unit-hyperscan*

使用 qemu-arm 运行的示例:

  1. $qemu-arm -L /opt/toolchain/crosstools-arm-gcc-5.3-linux-4.1-glibc-2.22-binutils-2.25/usr/arm-buildroot-linux-gnueabi/sysroot/ -E LD_LIBRARY_PATH=../lib ./unit-hyperscan

运行结果示例:
@POQXEMP4)}7SO_0G1N94MP.png

参考

hyperscan: 移植到ARM - 图2
本作品采用知识共享署名-非商业性使用-禁止演绎 3.0 未本地化版本许可协议进行许可。