※ 이 글은 오가사와라 히로유키(小笠原博之) 씨가 블로그에 적은 글을 번역한 것입니다. 사정에 따라 예고없이 삭제될 수 있으므로 양해부탁드립니다.

Android NDK r9b와 ARMv7A의 hard-float

(원문 : Android NDK r9b と ARMv7A の hard-float)

Android 4.4(KitKat)과 함께 NDK r9b가 릴리즈되었습니다. RenderScript 대응 등 몇가지 기능이 새로 추가되었습니다만, 그 중 ARMv7A의 hard-float 대응이 포함되어있습니다. 마침 NDK를 사용해서 함수계산기 앱을 만들던 중이었기에 시험해봤습니다.

  • Android NDK
  • ChotCalculator
  • Android/iOS등 ARM기기에서는 지금까지 float ABI로 softfp가 이용되었습니다. 함수 호출등의 인수는, 부동소수점일지라도 반드시 정수 레지스터 r을 경유하여, FPU의 유무와 상관없이 공통화할 수 있게 되어있습니다.

    그 대신 VFP가 탑재되어 있는 기기에서의 실행효율과 코드효율이 약간 희생되었습니다. 지금까지의 iOS/Android 스마트폰에서 VFP가 탑재되지 않은 것은 MSM7225 등 일부 ARM11(ARMv6, Android에서는 ARMv5TE)로 한정되어 있었고, ARMv7A에는 존재하지 않아 softfp를 이용할 필요는 없었습니다.

    Android NDK r9b에서는 hard-float에 대응하므로 VFP/NEON 레지스터를 직접 이용한 함수 호출이 가능하게 되었습니다.

    컴파일 방법

    NDK에서 hard-float를 지정하는 수순은 다음과 같습니다. 먼저 Android.mk에 추가합니다.

    LOCAL_CFLAGS += -mhard-float
    LOCAL_LDFLAGS += -Wl,--no-warn-mismatch

    단 대응하는 컴파일러는 gcc뿐으로 clang에서는 아직 사용할 수 없습니다. 이 옵션을 지정할 수 있는 것은 ARMv7A ( TARGET_ARCH_ABI = armeabi-v7a )의 경우 뿐입니다.

    외부 라이브러리의 선언

    시스템이나 외부 라이브러리는 softfp로 컴파일되므로, 컴파일러에 ABI가 다르다는 것을 올바르게 인식시킬 필요가 있습니다. NDK r9b 부속의 헤더에는 각 함수에 아래와 같은 선언이 추가되어 있습니다. 이것은 softfp 함수호출이라는 것을 의미합니다.

    __attribute__((pcs("aapcs")))

    예를 들어 NDK 부속의 math.h 헤더를 보면 다음과 같이 선언되어 있습니다.

    // math.h에서 발췌
    double	acos(double) __NDK_FPABI_MATH__;
    double	asin(double) __NDK_FPABI_MATH__;

    __NDK_FPABI_MATH__나 __NDK_FPABI__는 sys/cdefs.h에 정의되어 있습니다.

    // sys/cdefs.h에서 일부 발췌
    #define __NDK_FPABI__ __attribute__((pcs("aapcs")))
    #define __NDK_FPABI_MATH__ __NDK_FPABI__

    컴파일 결과와 libm의 hard_floag

    실제로 hard-float에서 컴파일한 결과는 아래와 같습니다. 로컬 함수 호출은 직접 d0 레지스터를 이용한다는 것을 알 수 있습니다.

    // hard-float
    // t_value f_bittof( t_value val )
    
       vcvt.u32.f64 s0, d0
       vcvt.f64.f32 d0, s0
       bx           lr

    ↓지금까지의 softfp에서 컴파일하면 다음과 같습니다. 64bit 배정밀도 부동소수는 r0/r1와 2개의 32bit 정수 레지스터를 사용하여 전달받고 있습니다.

    // softfp
    // t_value f_bittof( t_value val )
    
        vmov            d6, r0, r1
        vcvt.u32.f64    s15, d6
        vcvt.f64.f32    d6, s15
        vmov            r0, r1, d6
        bx              lr

    hard-float에서 컴파일한 경우에도, 외부 라이브러리 호출에서는 아래와 같이 r0/r1 레지스터로의 복사가 발생합니다.

    // hard-float (-lm)
    
        vmov    r0, r1, d0
        bl      0 
        vmov    d0, r0, r1

    단 libm은 hard-float에서 컴파일한 static 라이브러리가 부석되어 있으므로, 직접 VFP/NEON 레지스터에 의한 호출도 가능합니다.

    LOCAL_CFLAGS += -mhard-float -D_NDK_MATH_NO_SOFTFP=1
    LOCAL_LDFLAGS += -Wl,--no-warn-mismatch -lm_hard

    -lm (libm) 대신에 -lm_hard (libm_hard)로 지정합니다. 이 경우 헤더의 attribute 선언을 제거할 필요가 있으므로 -D_NDK_MATH_NO_SOFTFP=1 도 필요합니다. ↓ libm 호출에서도 레지스터 전송이 사라졌습니다.

    // hard-float (-lm_hard)
    
        b       0 

    애플리케이션 코드는 작아졌지만 libm_hard는 static이므로 프로그램 코드 전체는 늘어날 수 있습니다.

    libm 이외의 라이브러리 호출

    부동소수점을 이용하는 라이브러리는 libm 이외에도 존재합니다. 예를 들어 OpenGL ES 2.0 함수에도 attribute 선언이 추가되어 있습니다.

    // GLES2/gl2.h에서
    GL_APICALL void         GL_APIENTRY glClearColor (GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);
    
    // GLES2/gl2platform.h
    #define GL_APICALL  KHRONOS_APICALL
    
    // KHR/khrplatform.h에서 발췌
    #   define KHRONOS_APICALL __attribute__((visibility("default"))) __NDK_FPABI__

    실제로 glClearColor()를 호출해보면 hard-float에서도 r0-r3로의 복사가 이루어지고 있음을 알 수 있습니다.

    // hard-float (debug build)
    
        vstr    s0, [fp, #-8]
        vstr    s1, [fp, #-12]
        vstr    s2, [fp, #-16]
        vstr    s3, [fp, #-20]	; 0xffffffec
        ldr     r0, [fp, #-8]
        ldr     r1, [fp, #-12]
        ldr     r2, [fp, #-16]
        ldr     r3, [fp, #-20]
        bl      0 

    libm 이외에는 특별히 hard-float판이 준비되어있지는 않기에 softfp와 같은 전송이 이루어집니다.

    실제 애플리케이션에서 어느 정도의 차이가 날지는 알 수 없습니다만, NDK에서 더욱 최적화할 여지가 생겼습니다. 애초에 iOS 쪽은 이미 ARMv8로 이행하고 있습니다. Android도 64bit화가 이루어지면 이정도 차이를 의식할 필요는 없어질 듯 합니다.

    관련글

+ Recent posts