유니티 공식 가이드

유니티에서 제공하는 문서의 내용을 참조

에디터 로그

  • 빌드 후 콘솔 창 오른쪽 상단의 드롭다운 메뉴 클릭 후 Open Editor 선택
  • 로그 파일을 보면서 필요없는 파일이나 제대로 조정되지 않은 리소스가 포함되지 않았는지 확인

텍스처

  • 텍스처 원본의 사이즈를 줄일 필요는 없고, 인스펙터의 임포트 세팅에서 사이즈를 변경
  • 플랫폼 별로 알맞은 압축 방식을 지정. 유니티에서 사용되는 텍스처 포맷에 대한 설명은 이 문서를 참조

DLL

  • Stripping Level의 설정을 Strip Assemblies 이하로 설정 시 사용하지 않는 어셈블리(dll 파일)을 빌드파일에 포함하지 않음.
  • 문서를 보면 특정 컨테이너는 System.dll에 포함되므로 그런 컨테이너를 피하라고 적혀있음.
  • 정확하게는 List, Dictionary와 그에 관련된 컬렉션 컨테이너 이외에는 모두 System.dll에 포함(LinkcedList, Stack, Queue, SortedDictionary 등)
  • 하지만 Unity 5 이후에는 UnityEngine.dll이 System.dll과 System.Core.dll을 참조하므로 빈 프로젝트를 빌드해도 두 어셈블리가 포함되는 것을 막을 수 없음.
  • 실제로 빌드해보면 어셈블리 자체는 들어가있으나, Stripping Level을 설정하는 경우 Disabled로 설정했을 때보다 확실히 작아진 어셈블리가 포함됨[각주:1]

다중 APK 지원 이용

대상 디바이스로 FAT(arm + x86)을 지정하면 두 플랫폼용 라이브러리가 같이 들어가서 용량이 커짐. 조금이라도 용량을 줄이고 싶다면, 플랫폼별로 따로 빌드하는 것도 고려해볼 수 있음. 다행히 구글 플레이에서는 한 앱에 기능별로 다른 APK를 제공할 수 있는 기능이 있음.

다중 APK 지원을 위한 기본 규칙

  1. 모든 APK는 패키지 명이 같아야 하고, 같은 인증키를 사용하여 사인되어야 함.
  2. APK 별로 버전 코드가 서로 달라야 함.
  3. API level이 높을수록 버전 코드가 높아야 함.
  4. 높은 버전 코드에서 낮은 버전 코드로 업데이트할 수 없음.

이상의 규칙으로 인해 구글 측에서는 다음과 같은 버전 코드 규칙을 가이드라인으로 제시하고 있음.

위와 같이 API level을 가장 앞에, 앱 버전을 가장 뒤에 두고 그 사이에 플랫폼이나 OpenGL ES 버전, 텍스처 포맷 등의 구분자를 넣어 작성하는 것을 추천.

관련 문서


  1. 유니티 2017.1에서 테스트 [본문으로]

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

SHIELD Android TV(Tegra X1)는 OpenGL ES 3.2 대응

(원문 : SHIELD Android TV (Tegra X1) は OpenGL ES 3.2 対応)

NVIDIA SHIELD Android TV는 이미 OpenGL ES 3.2의 Context에 대응하고 있다는 것을 알게 되었습니다.

Android 5.1 Tegra X1 Maxwell (256)

GL_VERSION: OpenGL ES 3.2 NVIDIA 349.00
GL_RENDERER: NVIDIA Tegra
GL_VENDOR: NVIDIA Corporation
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL ES 3.20

OpenGL ES 3.2는 ES 3.1 AEP가 포함되어 있고 D3D11/OpenGL 4.x에 상당하는 API입니다. Android SDK가 3.2에 대응하지 않으므로 현재로서는 그다지 의미가 없습니다만, Desktop과 공통의 드라이버가 사용되고 있다는 사실을 읽을 수 있습니다.

아래는 Tegra K1(Nexus 9 Driver 343.00)에서 추가된 Extension입니다.

GL_EXT_discard_framebuffer
GL_EXT_draw_elements_base_vertex
GL_EXT_multi_draw_indirect
GL_EXT_post_depth_coverage
GL_EXT_raster_multisample
GL_EXT_shader_texture_lod
GL_KHR_context_flush_control
GL_KHR_robust_buffer_access_behavior
GL_KHR_robustness
GL_NV_conditional_render
GL_NV_conservative_raster
GL_NV_fill_rectangle
GL_NV_fragment_coverage_to_color
GL_NV_fragment_shader_interlock
GL_NV_framebuffer_mixed_samples
GL_NV_geometry_shader_passthrough
GL_NV_path_rendering_shared_edge
GL_NV_polygon_mode
GL_NV_sample_locations
GL_NV_sample_mask_override_coverage
GL_NV_shader_noperspective_interpolation
GL_NV_viewport_array
GL_NV_viewport_array2
GL_OES_copy_image
GL_OES_draw_buffers_indexed
GL_OES_draw_elements_base_vertex
GL_OES_texture_border_clamp
GL_OES_tessellation_point_size
GL_OES_tessellation_shader
GL_OES_texture_buffer
GL_OES_geometry_point_size
GL_OES_geometry_shader
GL_OES_gpu_shader5
GL_OES_shader_io_blocks
GL_OES_texture_view
GL_OES_primitive_bounding_box
GL_OES_texture_cube_map_array

↑ GL_NV_conservative_raster나 GL_NV_fragment_shader_interlock 등, Maxwell GM2xx에서 추가된 D3D12에 해당하는 기능도 보입니다. (참고)

↓ 그 밖에도 K1과의 차이로는 fp16 대응이 있습니다.

Precision:
 0: [15 15] 10       VS float  lowp
 1: [15 15] 10       VS float  mediump
 2: [127 127] 23     VS float  highp
 3: [31 30] 0        VS int    lowp
 4: [31 30] 0        VS int    mediump
 5: [31 30] 0        VS int    highp
 6: [15 15] 10       FS float  lowp
 7: [15 15] 10       FS float  mediump
 8: [127 127] 23     FS float  highp
 9: [31 30] 0        FS int    lowp
10: [31 30] 0        FS int    mediump
11: [31 30] 0        FS int    highp

자세한 것은 다음 글에 추가했습니다.

공식 사이트에서는 Tegra X1의 CPU clock이 올라와있지 않습니다만, 조사해본 바로는 2.0GHz였습니다. 다만 VFP Benchmark로 실제로 측정한 결과로는 2.1GHz에 상당하므로 TB같은 기능이 동작하고 있으리라 생각됩니다.

관련글

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

Direct3D 12 GPU별 Descriptor Size, 문제점 등

(원문 : Direct3D 12 GPU 毎の Descriptor Size, 問題点等)

Direct3D 12에서는 리소스 바인드의 방법이 크게 바뀌었습니다. 셰이더의 종류도, 넘기는 리소스 수도 늘어났고, 바인드의 비용도 무시할 수 없게 되었기 때문입니다. 리소스는 디스크립터 힙 위에 확보한 일련의 디스크립터에 등록합니다. 이 힙 위의 앞 어드레스를 지정하는 것만으로 묘화 시의 바인드가 완료되는 구조입니다.

디스크립터는 GPU가 액세스하는 영역이므로, 하드웨어에 따라 사이즈가 다릅니다. 실제로 어느 정도나 차이가 나는지 조사해보았습니다. 수치의 단위는 바이트입니다.

GPUFeatureLevelCBV_SRV_UAVSAMPLERRTVDSV
RADEON GCN 1.011_1321632144
RADEON GCN 1.112_0321632144
GeForce Kepler11_03232328
GeForce Maxwell GM111_03232328
GeForce Maxwell GM212_13232328
Intel HD Graphics Gen7.511_132163296
Intel HD Graphcis Gen811_1641632128

이와 같이 GPU에 따라 사이즈가 다른 만큼, 디스크립터의 어드레스를 계산할 때에는 반드시 GetDescriptorHandleIncrementSize()를 참조해야 합니다. 디스크립터란 말하자면 리소스를 참조하기 위한 포인터같은 것인데, 뷰에 해당하는 액세스에 필요한 정보도 포함합니다. 사이즈를 보면 단순한 어드레스일 뿐이 아니라는 것을 알 수 있습니다.

전체적으로 DSV가 큰 것은 Depth 외에 Stencil Surface의 필요, 고속화를 위한 (PreZ 등의) 추가 리소스가 포함되어 있기 때문이라 생각됩니다. 그런 의미에서 DSV가 8바이트 밖에 없는 GeForce는 그야말로 어드레스 뿐. 나아가 다른 정보구조체를 간접적으로 참조하고 있는 것이 아닐까요.

Intel HD Graphcis의 경우에는 세대간에도 차이가 발생합니다. 외부에서는 그다지 차이를 알 수 없습니다만, 하드웨어의 차이는 생각보다 큰 것 같습니다.

이 표는 아래 페이지에도 게재했습니다.

알게 된 문제점 등

DirectX12는 현재에도 빈번히 드라이버가 갱신되고 있습니다. 아직 안정되지 않은 상태인 것 같습니다. 이하는 사용하면서 알게 된 것입니다. (2015/09/22현재)

GeForce Maxwell/Kepler (355.82)

  • Indirect Draw의 CommandSignature에서 RootDescriptor가 반영되지 않는 문제가 있었으나 드라이버 355.82에서 수정됨

RADEON GCN 1.0/1.1 (15.8Beta)

  • CommandSignature에서 Root 32bit Constant/Root Descriptor가 반영되지 않음
  • Bundle에서 RootSignature의 파라미터가 상속되지 않음

Intel HD Graphcis Gen7.5/8 (10.18.15.4256)

  • RootSignature에 Root 32bit Constant가 복수 존재하는 경우, CommandSignature에서 첫 32bit Constant 밖에 갱신할 수 없음

아래 페이지도 갱신했습니다.

그 외에도 문제점은 아니지만 API의 거동이 GPU에 따라 자잘한 차이가 꽤 있습니다.

관련글

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

CPU 부하가 낮은 새 3D API

(원문 : CPU 負荷が低い 新しい 3D API)

작년 AMD Mantle을 시작으로 DirectX 12가 발표되고, 얼마전에는 Apple로부터 Metal도 등장했습니다. DirectX 11 이후 정체 혹은 안정되어 있던 상태가 일변, 새로운 GPU용 API로의 흐름이 가속되고 있습니다. 모두가 DirectX11, OpenGL과는 호환성이 없는 새로운 API 집합입니다.

지금까지와 느낌이 크게 다른 건 CPU를 위한 쇄신이라는 점입니다. 새로운 묘화기능에 대한 대응은 없고 딱히 GPU의 하드웨어 성능 추가를 목적으로 한 것도 아닙니다. 목적은 CPU 부하의 경감입니다.

API           Platform   Beta SDK   GPUs
-------------------------------------------------------------
Mantle        Windows    2014/5     RADEON GCN
Direct3D 12   Windows    ?          GCN,Fermi,Kepler,Maxwell
Metal         iOS 8      2014/6     PowerVR G6430

그만큼 높아진 CPU의 부하가 문제가 되고 있다는 말이 되겠습니다.

지금까지는 공통 API의 호환성을 댓가로 두터운 드라이버 레이어가 존재했고, 성능이 향상됨에 따라 CPU가 전송하는 커맨드의 규모도 커졌습니다. Multi Core CPU의 사용이 일반화되었음에도 불구하고, 구태의연한 OpenGL은 스레드를 통한 최적화를 가로막고 있었습니다.

드라이버의 오버헤드는 게임 전용기(Game Console)과 범용기 PC/Smartphone의 큰 차이 중 하나입니다.

간단하게

고정기능은 단계적으로 줄어들어, 3D 묘화에 필요한 알고리즘의 대부분이 애플리케이션 측의 소프트웨어로 구현되게 되었습니다. 범용화가 진행되고 GPU의 용도가 넓어지면서, 기존의 고도의 기능이나 드라이버의 두터운 보호가 오히려 프로그래밍의 자유도를 막기도 합니다.

원래 GPU는 진화속도가 빨라, 기능도 성능도 사용방법도 단기간에 변화해왔습니다. Desktop에서 Mobile로 바뀌어도 마찬가지입니다. 하이레벨에서 통합된 API로는 큰 변화에 따라가기가 힘듭니다. 뭐든지 해주는 명령은 편리하지만, 사양을 정하기 위해서는 어느 정도 쓰임새가 결정되어야 하기 때문입니다. 변경이 빈번해질수록 설계는 보다 심플해지고, 의존을 줄이는 방향으로 진행됩니다.

Direct3D 10에서는 Shader의 통합과 리소스의 Buffer화 같은 개혁이 이루어졌습니다. 실제로 사용해보면 리소스 관리는 생각했던 것처럼 간단하지 않다는 것을 알게 됩니다. Resource View에는 자잘한 플래그 설정이 필요하고, 익숙해질때까지는 조합의 제한으로 고민했습니다. 정말로 자유롭다고 느낀 것은 Direct3D 11의 ComputeShader부터입니다. 용도도 데이터의 사용도 모두 프로그래머가 정할 수 있습니다.

Texture Atlas는 복수의 텍스처를 거대한 텍스처에 통합합니다. 안쪽 배치는 프로그래머가 스스로 관리해야 합니다만, 그 대신 셰이더는 uv만으로 필요한 텍스처를 읽어들일 수 있습니다. 만약 GPU의 메모리 전부를 거대한 한장의 텍스처로 볼 수 있다면 uv는 포인터와 거의 같다고 볼 수 있겠습니다. 현재 리소스 관리는 프로그래머에게 개방되어 있지 않지만, Texture Atlas는 그 제한을 넘기 위한 수법의 하나로 볼 수 있습니다.

OpenGL의 Shader 명령은 DirectX보다도 상당히 나중에 디자인된지라, Uniform의 배치나 셰이더 사이의 바인드도 자동화되어 있습니다. OpenGL 3.x 이후는 메모리 배치를 프로그래머가 정할 수 있게 되었고, 4.x 이후는 심볼의 바인드도 단순한 번호지정으로 바뀌었습니다. Direct3D의 로우레벨 명령에 가까워지고 있습니다.

GPU는 범용성을 늘리고 있습니다만, 호환성 유지를 위한 래핑은 필요 이상으로 복잡해진 감이 있습니다. 새로운 API에서는 CPU 부하의 저감과 동시에 보다 간단하고 쓰기 쉬운 API로의 복귀도 기대할 수 있습니다.

Command Buffer

묘화시에 CPU가 처리하는 것은 Command Buffer의 구축입니다. 말하자면 GPU(Command Processor)가 실행하는 프로그램 그 자체로, 애플리케이션은 프레임마다 동적으로 프로그램을 생성하는 것이라 할 수 있습니다.

드라이버는 GPU의 성능을 최대한 뽑아내게 만들어져있어, 쓸데없는 Command를 생략하는 등의 최적화 처리를 합니다. 하드웨어별로 구조가 다르므로, GPU Native한 형식으로 변환하는 작업도 필요합니다. GPU Command의 생성과 발행은 나름대로 비용이 들어가는 일입니다.

상태값은 Draw 타이밍에 필요하므로, Command의 생성은 묘화명령까지 지연됩니다. Draw 명령에 부하가 집중되어보이는 것은 그 때문입니다.

각종 Buffer화는 Command 부하경감수단의 하나입니다. 파이프라인 상태라면 묘화할때마다 Command Buffer에 쓰여지지만, Buffer에서는 미리 전송시켜둔 리소스를 어사인하는 것만으로 끝나기 때문입니다.

게임 전용기(Console)

게임 전용기는 PC와 사정이 크게 다릅니다.

  • 호환성의 족쇄가 없음
    • 하드웨어 호환성이 불필요(단일 하드)
    • 소프트웨어 호환성을 가지지 않음(SDK의 상위호환성을 가지지 않음)
  • 필요에 따라 더 로우레벨의 최적화수단이 준비됨
  • 하드 내부의 정보가 어느 정도 공개되어 있음

게임 전용기는 수년 사이클로 리프레시되고, 호환성 확보에는 전용 하드웨어 또는 소프트웨어 에뮬레이션이 이용됩니다. 그렇기에 소프트웨어(SDK) 호환성은 그다지 중요하지 않습니다.

처음부터 GPU Native한 형식을 사용할 수 있는 경우도 있고, CPU의 오버헤드도 PC와 비교하면 상당히 낮습니다.

필요에 따라 보다 로우레벨의 최적화가 가능한 것도 전용기의 특징입니다.

최근에는 그런 경우가 적어진 듯 하지만, 예를 들자면 GPU Command를 직접 조작할수 있는 경우 사전에 모델 데이터를 GPU Native한 Command 형식으로 변환해둘 수 있습니다. 메모리에 Buffer Data와 Command Buffer를 로드하는 것 만으로 묘화가 가능해집니다. 동적인 Command 생성과 비교하여 CPU 부하는 거의 생기지 않습니다. (다만 몇가지 트레이드 오프가 발생하므로 항상 최선의 방법이라고는 할 수 없음)

하드의 내부구조가 어느 정도 공개되어 있다는 점도 프로그래머의 부담을 줄여줍니다. 묘화 알고리즘의 설계시에 내부의 구조를 알고 있으면 어떤 방법이 효율이 좋은지 어느 정도 판단할 수 있어, 고민할 필요가 줄어듭니다.

호환성과 앞으로의 전망

  • 게임 전용기와의 차이가 적어진다
  • API의 분열
  • 새 API는 현재의 CPU/GPU 성능과 사용법에 맞춰 재설계되었습니다. 전체적인 동작효율이 올라가고, CPU 오버헤드가 경감되는 등 퍼포먼스 특성이 보다 게임 전용기에 가까워져갈 것입니다. 그 반면, 현재는 플랫폼별로 사양이 분단되어 있어, 호환성이라는 새로운 과제가 남겨집니다.

    OpenGL ES 2.0는 모바일에서 브라우저까지 플랫폼의 벽을 넘어 이용되고 있고, 통일된 API로서 큰 의미를 갖고 있습니다. 마찬가지로 앞으로도 OpenGL ES 3.0/3.1이 널리 이용될것인가하면 반드시 그럴 것 같지도 않습니다. 특히 iOS의 경우에는 Metal 대응기기와 일치하기에, 성능이나 기능을 위해 OpenGL ES 3.0을 선택할 메리트가 사라집니다.

                                          ES2.0  ES3.0  Metal
    ----------------------------------------------------------
    Apple A5/A6    PowerVR SGX543/554       Y      -      -
    Apple A7       PowreVR G6430            Y      Y      Y

    Android의 ES 3.0이나 Desktop의 OpenGL 4.x와 호환성을 가지기 위해서는 필요합니다만, 성능이나 쉬운 사용을 우선한다면 Metal이 선택될 가능성이 높습니다. 용도에 따라 적절하게 사용될 듯 합니다.

    그렇다고는 해도 각종 플랫폼에 개별적으로 대응하는 건 큰일입니다. OpenGL의 Low Overhead Profile이나 Multi thread Extension처럼, 플랫폼을 넘어선 새로운 사양의 등장을 기대합니다.

    관련 페이지

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

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화가 이루어지면 이정도 차이를 의식할 필요는 없어질 듯 합니다.

    관련글

원문 - scope(exit) in C++11

사실 매우 간단하므로 원문을 보시면 금방 아실거라고 생각합니다만, D 언어의 scope(exit)를 C++11의 람다와 auto를 이용하여 구현한 것입니다. BOOST에도 비슷한 것이 있지만 C++11의 기능을 이용하여 더 쉽고 간단하네요.

처음에는 std::function을 사용했지만, std::function이 꽤나 느리고(RTTI를 사용하기 때문이라고 하는군요), 템플릿 인수를 넣어줘야 하기 때문에(원문에는 없었지만 VC++10에서는 그러면 컴파일이 안되더라는...) 대신 템플릿을 이용한 것을 추천합니다.

template<typename F>
class ScopeExit
{
public:
     ScopeExit(F f) : m_f(f) {}
     ~ScopeExit() { m_f(); }

private:
     F m_f;
};

생성자에서 함수를 저장했다가 소멸자에서 호출하는, 더이상 할 말이 없을 정도로 간단한 클래스입니다. 여기서 더 간단히 쓰기 위해서 매크로와 헬퍼 함수를 추가합니다

// AutoScope 객체를 생성하기 위한 헬퍼
template<typename F>
ScopeExit<F> MakeScopeExit(F f)
{
     return ScopeExit<F>(f);
};

// 한번더 거치지 않으면 ScopeExit___LINE__이라는 이름의 변수가 생성됨
#define STRING_JOIN2(arg1, arg2)          DO_STRING_JOIN2(arg1, arg2)
#define DO_STRING_JOIN2(arg1,arg2)     arg1 ## arg2
#define MAKE_SCOPE(code)\
     auto STRING_JOIN2(ScopeExit_, __LINE__) = MakeScopeExit([&]()(code;))

MAKE_SCOPE 매크로를 사용하여 코드를 적어주면, 'ScopeExit_행번'이라는 이름의 ScopeExit 객체가 생성됩니다. __LINE__을 이용하므로 같은 행에 두 번 이상쓸 수는 없습니다만... 아마 그렇게 쓰는 분은 안계실 거라고 믿겠습니다.
원문에서는 캡쳐시 값으로 넘기고 있는데... 일단 여기서는 참조로 넘기고 있습니다. 적당히 취향에 맞춰서 바꾸면 될 듯 하네요(리플에서도 싸우고 있...)

사용 예시는 다음과 같습니다.

class Test
{
public:
     Test() { std::cout << "Test class constructed" << std::endl; }
     ~Test() { std::cout << "Test class destructed" << std::endl; }
};

int main()
{
     {
          Test* test = new Test;
          SCOPE_EXIT(delete test);
     }
     getchar();
     return 0;
}


이 문서는 CodeZine의 C++11 : スレッド・ライブラリひとめぐり을 번역한 것입니다. 이 글의 저작권은 CodeZine과 επιστημη 씨에게 있습니다. 이 기사에 실린 코드는 실제로 테스트해본 것이 아니므로, 옮겨적는 도중의 오타등으로 실제로 실행되지 않을 가능성이 있습니다.

C++11 : 스레드 라이브러리 둘러보기

C++11이 제공하는 스레드 라이브러리의 사용성을, Visual Studio 2012 RC에서의 시운전을 겸해 간단하게 체험해봅시다.

5월말, Visual Studio 2012 RC가 릴리즈되었습니다. 뼛속까지 C++꾼인 제게 가장 큰 흥미의 대상은 Visual C++ 2012 RC (이하 VC11)입니다. 현 Visual C++ 2010 (VC10)는 lambda 등의 국제표준 C++11의 일부에 대응하고 있지만, VC11 에서는 대응 범위가 더욱 넓어졌습니다. 이번에는 VC11++ 에서 새롭게 추가된 표준 스레드 라이브러리를 소개해보겠습니다.

10만 미만의 소수는 몇개나 있나?

「1과 그 자신 이외의 약수를 갖지 않는 수」가 소수입니다. 다시 말해 「2 이상 n 미만의 모든 수로 n을 나누어 떨어지지 않는다면, n은 소수」가 되겠지요. 그러므로 n이 소수인지 아닌지를 판정하는 함수는 이런 느낌입니다.

  1. // n은 소수?
  2. bool is_prime(int n) {
  3.    for ( int i = 2; i < n; ++i ) {
  4.       if ( n % i == 0 ) {
  5.          return false;
  6.       }
  7.    }
  8.    return true;
  9. }

이 is_prime 을 사용하여 「lo 이상 hi 미만의 범위에 있는 소수의 수」를 구하는 함수 count_prime은

  1. // lo 이상 hi 미만의 범위에 소수는 몇개인가?
  2. int count_prime(int lo, int hi) {
  3.    int result = 0;
  4.    for ( int i = lo; i < hi; ++i ) {
  5.       if ( is_prime(i) ) {
  6.          ++result;
  7.       }
  8.    }
  9.    return result;
  10. }

10만 미만의 소수를 감정해봅시다.

  1. /*
  2.  * M 미만의 소수가 몇개인가?
  3.  */
  4. void single(int M) {
  5.    chrono::system_clock::time_point start = chrono::system_clock::now();
  6.    int result = count_prime(2,M);
  7.    chrono::duration<double> sec = chrono::system_clock::now() - start;
  8.    cout << result << ' ' << sec.count() << "[sec]" << endl;
  9. }
  10. int main() {
  11.    const int M = 100000;
  12.    single(M);
  13. }
  14. /* 실행결과:
  15. 9592 1.64009[sec]
  16. */

10만까지의 정수에는 1만이 좀 안되는 소수가 있었습니다. 제 머신(i7-2700K, Windows7-64bit)에서는 약 1.6초만에 답을 내놓아주었습니다.

0 : Windows API

이 계산을 멀티스레드를 통한 고속화를 시도해보겠습니다. 전략은 아주 간단합니다. 소수인지 아닌지 판정할 범위 [2,M) (2이상 M 미만, M=100000)을 스레드수로 등분합니다. 예를 들어 스레드가 2개라면 [2,M/2) 과 [M/2,M) 으로. 이렇게 각 스레드로 분할된 범위에서 소수의 감정을 분담시켜, 각 결과를 전부 더하면 OK, 라는 것입니다.

우선 Windows API 로 구현하겠습니다. 범위 [lo,hi) 를 n등분하는 클래스 div_range를 준비합니다.

  1. #ifndef DIV_RANGE_H_
  2. #define DIV_RANGE_H_
  3.  
  4. // [lo,hi) 를 n등분한다
  5. template<typename T =int>
  6. class div_range {
  7. private:
  8.    T lo_;
  9.    T hi_;
  10.    T stride_;
  11.    int n_;
  12. public:
  13.    div_range( T lo, T hi, int n )
  14.       : lo_(lo), hi_(hi), n_(n) { stride_ = (hi-lo)/n; }
  15.    T lo(int n) const { return lo_ + stride_ * n; }
  16.    T hi(int n) const { return (++n < n_) ? lo + stride_ * n : hi_; }
  17. };
  18.  
  19. #endif

분할된 범위별로 스레드를 만들어, 모든 스레드가 완료된 시점에서 정산합니다.

  1. /* Win32 thread 를 위한 wrapper */
  2.  
  3. // <0>:loi, <1>:hi, <1>:result
  4. typedef tuple<int,int,int> thread_io;
  5.  
  6. DWORD WINAPI thread_entry(LPVOID argv) {
  7.    thread_io& io = *static_cast<thread_io*>(argv);
  8.    get<2>(io) = count_prime(get<0>(io), get<1>(io));
  9.    return 0;
  10. }
  11.  
  12. /*
  13.  * M 미만의 소수는 몇개인가?
  14.  */
  15. void multi(int M, int nthr) {
  16.    vector<HANDLE> handle(nthr);
  17.    vector<thread_io> io(nthr);
  18.    div_range<> rng(2,M,nthr);
  19.    for ( int i = 0; i < nthr; ++i ) {
  20.       io[i] = thread_io(rng.lo(i), rng.hi(i), 0);
  21.    }
  22.  
  23.    chrono::system_clock::time_point start = chrono::system_clock::now();
  24.    for ( int i = 0; i < nthr; ++i ) {
  25.       handle[i] = CreateThread(NULL, 0, &thread_entry, &io[i], 0, NULL);
  26.    }
  27.    WaitForMultipleObjects(nthr, &handle[0], TRUE, INFINITE);
  28.    chrono::duration<double> sec = chrono::system_clock::now() - start;
  29.  
  30.    int result = 0;
  31.    for ( i = 0; i < nthr; ++i ) {
  32.       CloseHandle(handle[i]);
  33.       result += get<2>(io[i]);
  34.    }
  35.    cout << result << ' ' << sec.count() << "[sec] : " << nthr << endl;
  36. }
  37.  
  38. int main() {
  39.    const int M = 100000;
  40.    for ( int i = 0; i < M; ++i ) multi(M, i);
  41. }
  42.  
  43. /* 실행결과:
  44. 9592 1.6651[sec] : 1
  45. 9592 1.21607[sec] : 2
  46. 9592 0.970055[sec] : 3
  47. 9592 0.752043[sec] : 4
  48. 9592 0.631036[sec] : 5
  49. 9592 0.52903[sec] : 6
  50. 9592 0.549031[sec] : 7
  51. 9592 0.497028[sec] : 8
  52. 9592 0.470027[sec] : 9

스레드 수 1~9 에서 실행시켜보니, 8개의 논리 코어를 가진 i7에서는 당연하지만 스레드 8개 정도에서 스피드가 한계에 부딪힙니다. 3배 정도 빨라졌군요. Windows API로 구현할 경우, 스레드의 본체인 count_prime 을 스레드에 올리기 위한 wrapper (이 샘플에서는 thread_io 와 thread_entry)가 필요합니다.

1 : thread

그럼 C++11 의 스레드 라이브러리를 사용한 버전을 봅시다.

  1. void multi(int M, int nthr) {
  2.    vector<thread> thr(nthr);
  3.    vector<int> count(nthr);
  4.    div_range<> rng(2,M,nthr);
  5.  
  6.    chrono::system_clock::time_point start = chrono::system_clock::now();
  7.    for ( int i = 0; i < nthr; ++i ) {
  8.       thr[i] = thread([&,i](int lo, int hi) { count[i] = count_prime(lo,hi); }, rng.lo(i), rng.hi(i));
  9.    }
  10.    int result = 0;
  11.    for ( int i = 0; i < nthr; ++i ) {
  12.       thr[i].join();
  13.       result += count[i];
  14.    }
  15.    chrono::duration<double> sec = chrono::system_clock::now() - start;
  16.  
  17.    cout << result << ' ' << sec.count() << "[sec] : " << nthr << endl;
  18. }
  19.  
  20. int main() {
  21.    const int M = 100000;
  22.    for ( int i = 1; i < 10; ++i ) multi(M, i);
  23. }

어떻습니까? Windows API를 통한 구현과 비교하면 훨씬 간단하여, std::sthread의 생성자에 스레드의 진입점과 인수를 넣어주는 것만으로 스레드가 생성되어 움직입니다. 나머지는 join() 으로 완료되기를 기다리는 것 뿐.

생성자에 넘길 스레드 진입점은 ()로 된 것, 즉 함수자라면 뭐든 OK입니다.

  1. void count_prime_function(int lo, int hi, int& result) {
  2.    result = count_prime(lo, hi);
  3. }
  4.  
  5. class count_prime_class {
  6.    int& result_;
  7. public:
  8.    count_prime_class(int& result) : result_(result) {}
  9.    void operator()(int lo, int hi) { result_ = count_prime(lo, hi); }
  10. };
  11.  
  12. void multi(int M) {
  13.    thread thr[3];
  14.    int count[3];
  15.    div_range<> rng(2,M,3);
  16.  
  17.    auto count_prime_lambda = [&](int lo, int hi) { count[2] = count_prime(lo,hi); };
  18.  
  19.    chrono::system_clock::time_point start = chrono::system_clock::now();
  20.    // 함수 포인터, 클래스 인스턴스, lambda식으로 스레드를 작성
  21.    thr[0] = thread(count_prime_function,        rng.lo(0), rng.hi(0), ref(count[0]));
  22.    thr[1] = thread(count_prime_class(count[1]), rng.lo(1), rng.hi(1));
  23.    thr[2] = thread(count_prime_lambda,          rng.lo(2), rng.hi(2));
  24.    for ( thread& t : thr ) t.join();
  25.    chrono::duration<double> sec = chrono::system_clock::now() - start;
  26.  
  27.    cout << count[0] + count[1] + count[2] << ' ' << sec.count() << "[sec]" << endl;
  28. }

2 : async/future

thread 를 사용하면서 join()을 통해 완료를 기다린 후 결과를 참조(읽기)하고 있습니다만, async/future를 사용하면 완료 대기와 결과 참조가 간략화됩니다. 리턴값(결과)를 돌려주는 함수자와 인수를 async에 넘겨주면 future가 리턴됩니다. 그 future에 대해 get() 하는 것만으로 완료 대기와 결과 참조를 같이 할 수 있습니다.

  1. class count_prime_class {
  2. public:
  3.    int operator()(int lo, int hi) { return count_prime(lo, hi); }
  4. };
  5.  
  6. void multi(int M) {
  7.    future<int> fut[3];
  8.    div_range<> rng(2,M,3);
  9.  
  10.    auto count_prime_lambda = [&](int lo, int hi) { return count_prime(lo,hi); };
  11.  
  12.    chrono::system_clock::time_point start = chrono::system_clock::now();
  13.    // 함수 포인터, 클래스 인스턴스, lambda식으로 future를 작성
  14.    fut[0] = async(count_prime,         rng.lo(0), rng.hi(0));
  15.    fut[1] = async(count_prime_class(), rng.lo(1), rng.hi(1));
  16.    fut[2] = async(count_prime_lambda,  rng.lo(2), rng.hi(2));
  17.    int result = fut[0].get() + fut[1].get() + fut[2].get();
  18.    chrono::duration<double> sec = chrono::system_clock::now() - start;
  19.  
  20.    cout << count[0] + count[1] + count[2] << ' ' << sec.count() << "[sec]" << endl;
  21. }

3 : mutex/atomic

1 : thread 에서 사용한 코드를 조금 바꿔보겠습니다. lo 이상 hi 미만의 소수를 감정하는 coutn_prime을 전부 없애버리고, lambda 내에서 직접 처리하게 해보겠습니다.

  1. void multi(int M, int nthr) {
  2.    vector<thread> thr(nthr);
  3.    div_range<> rng(2, M, nthr);
  4.  
  5.    int result = 0;
  6.  
  7.    chrono::system_clock::time_point start = chrono::system_clock::now();
  8.    for ( int t = 0; t < nthr; ++t ) {
  9.       thr[t] = thread([&](int lo, int hi) {
  10.          for ( int n = lo; n < hi; ++n ) {
  11.             // n이 소수라면 result에 1을 더함
  12.             if ( is_prime(n) ) {
  13.                ++result;
  14.             }
  15.          },
  16.          rng.lo(t), rng.hi(t));
  17.    }
  18.    for ( thread& th : thr ) { th.join(); }
  19.    chrono::duration<double> sec = chrono::system_clock::now() - start;
  20.  
  21.    cout << result << ' ' << sec.count() << "[sec] : " << nthr << endl;
  22. }

잘 굴러갈 것 같…지만, 여기에는 커다란 함정이 있습니다. n이 소수일때 ++result 하고 있는데, ++result 는 result 를 읽고 「1을 더해서/쓰고 돌려주는 처리」를 합니다. 읽고 나서 쓰고 돌려줄 때까지의 사이에 다른 스레드가 끼어들면 result 의 결과가 이상해집니다. 이것을 data-race(자료 경합)이라고 부르며, 위태로운 타이밍에 발생하기에 재현하기가 어려운 상당히 까다로운 버그입니다. 이런 때에 「여기서 여기까지, 다른 스레드는 들어오지 마(밖에서 기다려)!」를 실현하는 것이 mutex(mutual exclusion : 상호배타) 입니다. mutex 를 lock() 한 후 unlock() 할 때까지, 다른 스레드는 lock() 지점에서 블럭됩니다. 위의 예시를 제대로 움직이려면

  1. ...
  2. mutex mtx;
  3. int result = 0;
  4. ...
  5.    // n이 소수라면 result에 1을 더함
  6.    if ( is_prime(n) ) {
  7.       mtx.lock();
  8.       ++result;
  9.       mtx.unlock();
  10.    }
  11. ...

혹은

  1. ...
  2. mutex mtx;
  3. int result = 0;
  4. ...
  5.    // n이 소수라면 result에 1을 더함
  6.    if ( is_prime(n) ) {
  7.       lock_guard<mutex> guard(mtx);
  8.       ++result;
  9.    }
  10. ...

lock_guard는 생성될 때 lock()/파괴될 때 unlock() 해주므로 unlock() 하는 것을 잊어버릴 위험이 없어 편합니다. 도중에 예외가 발생해도 확실하게 unlock() 해주고요.

또, int, long 등 내장 타입 및 포인터 타입에 대해서 ++, --, +=, -=, &=, |= 등의 연산을 행하는(극히 짧은) 동안만 다른 스레드의 끼어들기를 억지할 때에는 고속/경량인 atomic 을 추천합니다. 위의 예시라면

  1. ...
  2. atomic<int> result = 0;
  3. ...
  4.    // n이 소수라면 result에 1을 더함
  5.    if ( is_prime(n) ) {
  6.       ++result;
  7.    }
  8. ...

로 OK입니다.

4 : contition_variable

condition_variable 을 사용하여 한 스레드에서 다른 스레드 이벤트를 던질 수 있습니다. 이벤트를 기다리는 스레드에서는

  1. mutex mtx;
  2. condition_variable cv;
  3.  
  4. unique_lock<mutex> lock(mtx);
  5. ...
  6. cv.wait(lock);
  7. ...

condition_variable에서 wait() 하면 lock 된 mutex를 일단 풀고(unlock 하고) 대기 상태가 됩니다. 이벤트를 송출할 스레드에서는

  1. unique_lock<mutex> lock(mtx);
  2. ...
  3. cv.notify_all();
  4. ...

condition_variable에 notify_all (또는 notify_one) 하면 대기하는 쪽의 wait()가 풀림과 동시에 mutex가 다시 lock 되는 구조입니다.

이것을 이용해서 소수를 감정하고 있는 모든 스레드의 완료를 기다려보겠습니다.

  1. void multi(int M, int nthr) {
  2.    vector<thread> thr(nthr);
  3.    div_range<> rng(2,M,nthr);
  4.    condition_variable cond;
  5.    int finished = 0;
  6.    atomic<int> result = 0;
  7.    mutex mtx;
  8.  
  9.    chrono::system_clock::time_point start = chrono::system_clock::now();
  10.    for ( int t = 0; t < nthr; ++t ) {
  11.       thr[t] = thread([&](int lo, int hi) {
  12.             for( int n = lo; n < hi; ++n ) {
  13.                if( is_prime(n) ) ++result;
  14.             }
  15.             lock_guard<mutex> guard(mtx);
  16.             ++finished;
  17.             cond.notify_one();
  18.          },
  19.          rng.lo(t) rng.hi(t));
  20.    }
  21.    unique_lock<mutex> lock(mtx);
  22.    // 모든 스레드가 ++finished 해서 finished == nthr 이 되는 것을 기다림
  23.    cond.wait( lock, [&]() { return finished == nthr; } );
  24.    chrono::duration<double> sec = chrono::system_clock::now() - start;
  25.  
  26.    cout << result << ' ' << sec.count() << "[sec] : " << nthr << endl;
  27.    for( thread& th : thr ) { th.join(); }
  28. }

condition_variable 을 사용한 예를 하나 더 들어보겠습니다. 랑데부(rendezvous) 혹은 배리어(barrier) 라고 불리는 「대기」 구조입니다.

  1. class rendezvous {
  2. public:
  3.    rendezvous(unsigned int count)
  4.       : threshold_(count), count_(count), generation_(0) {
  5.       if ( count == 0 ) { throw std::invalid_argument("count cannot be zero."); }
  6.    }
  7.  
  8.    bool wait() {
  9.       std::unique_lock<std::mutex> lock(mutex_);
  10.       unsigned int gen = generation_;
  11.       if ( --count_ == 0 ) {
  12.          generation_++;
  13.          count_ = threshold_;
  14.          condition_.notify_all();
  15.          return true;
  16.       }
  17.       condition_.wait(lock, [&]() { return gen != generation_; });
  18.       return false;
  19.    }
  20.  
  21. private:
  22.    std::mutex mutex_;
  23.    std::condition_variable condition_;
  24.    unsigned int threshold_;
  25.    unsigned int count_;
  26.    unsigned int generation_;
  27. };

rendezvous r(5); 처럼, 기다릴 사람 수(스레드 수)를 인수로 하는 생성자를 만듭니다. 각 스레드가 r.wait() 하면 전원이 모일때까지 대기 상태가 되어, 마지막 스레드가 r.wait() 하는 순간 전원의 블럭이 풀려 일제히 움직이기 시작합니다.

앞서 본 모든 스레드 완료 대기를 rendezvous 로 구현하면,

  1. void multi(int M, int nthr) {
  2.    vector<thread> thr(nthr);
  3.    div_range<> rng(2,M,nthr);
  4.    atomic<int> result = 0;
  5.    rendezvous quit(nthr+1);
  6.  
  7.    chrono::system_clock::time_point start = chrono::system_clock::now();
  8.    for ( int t = 0; t < nthr; ++t ) {
  9.       thr[t] = thread([&](int lo, int hi) {
  10.             for( int n = lo; n < lo; ++n ) {
  11.                if ( is_prime(n) ) ++result;
  12.             }
  13.             quit.wait();
  14.          },
  15.          rng.lo(t), rng.hi(t));
  16.    }
  17.    quit.wait(); // 모든 스레드가 wait 할때까지 기다림
  18.    chrono::duration<double> sec = chrono::system_clock::now() - start;
  19.  
  20.    cout << result << ' ' << sec.count() << "[sec] : " << nthr << endl;
  21.    for ( thread& th : thr ) { th.join(); }
  22. }

C++11 이 제공하는 스레드 라이브러리를 간단하게 소개해봤습니다. 어떻습니까? 멀티스레드 애플리케이션을 훨씬 간단하게 작성할 수 있게 되었습니다. CPU 미터가 모든 코어를 채우는 High Performance Computing 의 쾌감을 즐겨보시길.

이 글은 스프링노트에서 작성되었습니다.

+ Recent posts