#!if top = 문서명1 != null ? 문서명1 : calleeTitle != null ? (i = calleeTitle.lastIndexOf("/")) != -1 ? calleeTitle.substr(0, i) : '상위 문서' : '상위 문서'
[[파일:상위 문서 아이콘.svg|align=left&width=21.6px]] {{{#!html }}} 상위 문서: [[]]1. 개요
OpenCL은 C99를 바탕으로 한 확장 언어로, 문법은 C와 같으나, 병렬처리 시스템에 맞는 확장된 문법을 가지고 있다.OpenCL 코드는 런타임을 초기화하는 Platform/Runtime 코드와 오프로드 장치에서 동작하는 커널로 이뤄져 있으며, 커널 코드를 동작시키기 위해서는 Platform 코드가 필요하다.
2. 코드 도메인
2.1. Platform 코드
Platform 코드는 장치를 초기화하거나 장치와 통신해 커널을 실행하기 위해 필수적인 런타임 기능을 가진다. 이 플랫폼은 ICD를 실행시키도록 되어 있으며 이를 통해 각 벤더가 자사의 디바이스에 맞는 장치를 관리할 수 있게 해 준다.2.1.1. cl.h와 opencl.hpp의 차이
C++ 래핑 버전인 OpenCL.hpp 헤더 (구 cl2.hpp) 의 경우 C++ STL 컨테이너에 맞춰 일일히 수동으로 처리해 줘야 하는 플랫폼 관련 코드들의 배열을 자동으로 처리해 주므로 필요한 소스코드의 길이가 적다.특히 CUDA나 oneAPI에 비해 OpenCL은 장치 초기화 단계가 꽤 긴 편 인데다, 기본인 C를 사용하면 이 처리를 온전히 직접 처리해야 하지만 C++ 레퍼 헤더 (OpenCL.hpp)를 사용하는 경우 보일러플레이트 코드가 획기적으로 줄어드므로 C가 꼭 필요한게 아니라면 C++ 쪽이 훨씬 간결하다.
다음 예시는 임의로 0번 플랫폼과 0번 디바이스를 사용하고 일부 메모리 할당과 예외 처리는 무시하였다.
#!syntax cpp
#include <CL/CL.h>
#include <stdlib.h>
int main()
{
cl_int clError = CL_SUCCESS;
// Platform Query
cl_uint numPlatforms = 0;
cl_platform_id *platforms = NULL;
clError = clGetPlatformIDs(0, NULL, &numPlatforms);
cl_platform_id platforms =
(cl_platform_id *)malloc(sizeof(cl_platform_id) * numPlatforms);
clGetPlatformIDs(numPlatforms, platforms, NULL);
cl_platform_id platform = platforms[0];
// Device Query
cl_device_id *devices = NULL;
cl_uint numDevices = 0;
clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, 0, NULL, &numDevices);
cl_device_id = (cl_device_id *)malloc(sizeof(cl_device_id) * numDevices);
clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, numDevices, devices, NULL);
cl_device_id device = devices[0];
char deviceName[128];
clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(deviceName)-1, deviceName, NULL);
deviceName;
}
#!syntax cpp
#include <CL/OpenCL.hpp>
#include <vector>
#include <string>
int main()
{
// Platform Query
std::vector<cl::Platform> platforms;
cl::Platform::get(&platforms);
cl::Platform platform = platforms[0];
// Device Query
std::vector<cl::Device> devices;
platform.getDevices(CL_DEVICE_TYPE_ALL, &devices);
cl::Device device = devices[0];
std::string deviceName = device.getInfo<CL_DEVICE_NAME>();
deviceName;
// ...
}
2.2. 커널
커널은 하드웨어에서 실제로 수행되는 코드이다. C99를 바탕으로 된 문법을 가지며__kernel 또는 kernel attribute 선언을 통해 호스트에서 진입할 수 있는 함수를 만들 수 있다. 그 외 코드는 디바이스 내부에서만 접근이 가능하다.#!syntax cpp
__kernel void
arrAdd(global const int *src1, global const int *src2, global int *dest)
{
const uint index = get_global_id(0);
dest[index] = src1[index] + src2[index];
return;
}
2.2.1. 자료형
기본 자료형은 다음을 지원한다.| 스칼라 |
bool, char, unsigned char, uchar, short, unsigned short, ushort, int, unsigned int, uint, long, unsigned long, ulong, float, double, half, size_t, ptrdiff_t, intptr_t, uintptr_t, void |
| 벡터 |
char<>, uchar<>, short<>, ushort<>, int<>, uint<>, long<>, ulong<>, half<>, float<>, double<> |
| 이미지형 |
image2d_t, image3d_t, image2d_array_t, image1d_t, image1d_buffer_t, image1d_array_t, image2d_depth_t, image2d_array_depth_t, sampler_t, queue_t, ndrange_t, clk_event_t, reserve_id_t, event_t, cl_mem_fence_flags, image2d_msaa_t, image2d_array_msaa_t, image2d_msaa_depth_t, image2d_array_msaa_depth_t |
| 예약 |
bool<>, quad, quad<>, complex/imaginary (half/float/double/quad)(<>), float<>x<>, double<>x<>, long double(<>), long long(<>), (unsigned long) (long/ulong)(<>) |
장치와 호스트간 엄격한 자료 교환이 필요한 경우 추가 자료형을 지원한다. 예를 들어 커널 코드에서 half 자료형을 사용하는 경우, 호스트 코드 (API)에서는 cl_half로 앞에 cl_ prefix를 붙이면 된다. 예외로 bool, size_t는 동치형이 없으며 ptrdiff_t와 같은 포인터 주소형도 지원하지 않는다.
half 반정밀도의 경우 IEEE 754 표준을 준수하므로 1S5E10M 구조를 가진다.
벡터형의 경우 연속된 레인에 값이 패킹되어 저장된다. 스칼라형 뒤의 <>에는 길이를 붙이면 되며 예를 들어
char4는 4개의 char를 하나로 다룰 수 있다.벡터 크기는 2,3,4,8,16 이 지원되며, 그 외는 지원하지 않는다.
이미지형의 경우 메모리 어레이를 일종의 추상화를 통해 접근하는 방식으로 호스트에서는 전부 cl_mem으로 접근한다.
스칼라형은 벡터형으로 캐스팅할 수 있다.
#!syntax cpp
(float4)(float, float, float, float); // 4개의 float을 1개의 float4 형으로
(float4)(float2, float, float); // 한개의 float2 벡터와 두개의 float 스칼라를 float 4 형으로
(float4)(float); // 한개의 float을 float4 로 브로드캐스팅
단 벡터는 자동으로 브로드캐스팅 되지 않으므로
(float4)(float2)는 오류다.OpenCL C 는 대체로 묵시적 형변환을 인정하므로 예를 들어 float에 int를 대입하는 동작은 허용된다.
벡터와 스칼라간 연산 또한 지원하며 이에 따라
#!syntax cpp
float4 v = (float4)u + (float)f는 다음과 같다. #!latex
\begin{bmatrix} v_x \\ v_y \\ v_z \\ v_w \end{bmatrix} = \begin{bmatrix} u_x \\ u_y \\ u_z \\ u_w \end{bmatrix} + \begin{bmatrix} f \\ f \\ f \\ f \end{bmatrix}
당연하지만 벡터와 벡터 연산도 가능하다. 이에 따라
#!syntax cpp (float4)w = (float4)v + (float4)u는 다음과 같다.#!latex
\begin{bmatrix} w.x \\ w.y \\ w.z \\ w.w \end{bmatrix} = \begin{bmatrix} v.x \\ v.y \\ v.z \\ v.w \end{bmatrix} + \begin{bmatrix} u.x \\ u.y \\ u.z \\ u.w \end{bmatrix} = \begin{bmatrix} v.x + u.x \\ v.y + u.y \\ v.z + u.z \\ v.w + u.w \end{bmatrix}
2.2.2. 벡터 접근자
벡터로 선언된 변수의 원소에 접근하는 경우 다음의 인덱스를 통해 접근하며 Swizzle과 shuffle 을 사용할 수 있다.xyzw, rgba(OpenCL C 3.0+), 0-9,a-f,A-F, lo, hi, odd, even#!syntax cpp
float4 myFloat4 = (float4)(0.1f, 0.2f, 0.3f, 0.4f);
myFloat4.x; // 0.1f
myFloat4.y; // 0.2f
myFloat4.xy; // (0.1f, 0.2f) -> float2;
myFloat4.zwz; // (0.3f, 0.4f, 0.3f) -> float3;
myFloat4.lo; // (0.1f, 0.2f) -> float2;
myFloat4.hi; // (0.3f, 0.4f) -> float2;
myFloat4.even; // myFloat4.xz - > float2;
myFloat4.odd; // myFloat4.yw -> float2;
myFloat4.0123; // myFloat4.xyzw -> float4;
단 이렇게 접근하는 경우 주소(레퍼런스)는 오류다.
#!syntax cpp &myFloat4.x;2.2.3. 지정자 (qualifiers)
| 주소 |
global, __global, __local, local, __constant, constant, __private, private, __generic, generic [1] |
| 함수 |
__kernel, kernel |
| 접근 |
__read_only read_only, __write_only, write_only, __read_write, read_write |
| 기타 |
uniform, pipe |
2.2.4. 연산자
+ - * & / << >> <<= >>= ? : < > = -- ++ , .2.2.5. 내장 함수
- 삼각함수와 초월함수
acos, acosh, acospi,...
내장 함수와 같은 역할을 하지만 하드웨어를 통한 근사값을 통해 속도를 높일 수 있는 함수들이 있다. 이 함수들은native_Prefix가 붙어 있으며 예를 들어native_rsqrt(), rsqrt()둘 다 역제곱근을 계산하지만, native의 경우 하드웨어에 내장된 유닛을 사용할 수도 있고, 룩업 테이블로 근사할 수도 있는 등 구현은 제조사 마음대로다. 대체로 계산 정밀도는 떨어지지만, 속도가 매우 빠르다.[2]
Native 함수를 안 써도 unsafe math 옵션을 사용하면 컴파일러가 전부 바꿔버리는데 성능과 정밀도가 둘 다 중요하다면 선택적으로 적용해 내장 함수를 직접 사용하는 것이 권장된다.
- vload<>, vstore<>
메모리 배열을 벡터 단위로 읽거나 쓴다. 예를 들어 메모리에 rgb 픽셀이 이어진 경우 vload3()을 통해 3개의 원소를 가진 값을 가져올 수 있다. 보통 최대의 효율을 위해서는 같은 메모리 접근이여도 스칼라 단위로 연산을 하는 것이 아니라 벡터 단위로 하는 것이 하드웨어의 효율을 최대한 이끌어 낼 수 있다. 스칼라로 작성해도 대충은 돌아 가긴 하고 컴파일러가 가능하면 알아서 잘 처리해 주긴 하지만 이는 매우 제한적이므로 명시적으로 벡터 단위로 접근하는것이 표준이다.
- printf
C의 printf 맞다. 보통 커널에서 동작하는 코드는 호스트와 독립적으로 동작하기에 디버깅이 까다롭지만 printf를 통해 값을 출력할 수 있다. 당연하지만 많이 쓰면 printf로 인한 block이 생기므로 디버깅용으로만 사용하는 것이 좋다. OpenCL 1.2 부터 표준화가 되었는데, 그 이전까지는 벤더의 자체 확장 (cl_amd_printf, cl_intel_printf, cl_arm_printf 등)에 의존했다. 워낙 유용한 기능이라 너도나도 자체 확장을 만들어 쓰다 보니 표준으로 편입된 기능이 되었다.
- get_global_id, get_local_id
GPGPU의 경우 데이터를 쪼개서 연산하게 되는데, 각 연산 코어가 위치한 순번을 조회할 때 사용한다. 예를 들어 32*32 크기의 데이터는 32개 유닛을 가진 프로세서에서 각 코어가 32개씩, 총 32코어가 1024개의 데이터를 병렬로 처리하는데, 코어가 위치한 그리드 도메인 내를 식별하기 위한 방법이다. 이게 없이는 메모리 접근이 거의 불가능하다고 보면 된다.