Tistory View

ComputeShader에는 2가지의 Barrier가 있다. 아하... 이 이야기를 하기전에 Barrier가 뭔지를 설명해야 겠구나..

 

Barrier란..?

일단 ComputeShader와는 상관없이 Barrier는 2가지 종류가 있다. 하나는 execution barrier, 나머지는 memory barrier다.

CPU든 GPU든 이런 것들이 계산과 처리를 할 때, (여기서 CPU와 GPU같은 것이 만들어진 구조를 아키텍처[그냥단어다]라고 부른다.) 특히 멀티쓰레드로 작업을 할 때, 이 멀티쓰레드 작업의 결과가 올바르게 나오기 위해, 때로는 쓰레드의 실행을 멈추었다가 다시 실행해야 되고, 때로는 메모리 쓰여진 데이터가 다른 쓰레드에서 이 쓰여진 데이터를 읽어야 하는 경우가 있다.

실행을 멈추고 다시 실행을 계속하는 과정에서 실행을 멈추는 것을 execution barrier라고 하고, 쓰여진 데이터를 다른 쓰레드가 올바르게 읽게 하기위해 cache데이터를 메모리로 전송완료 시키는 것을 memory barrier라고 한다. 이 때 보통 다음에 처리할 쓰레드들은 완료시까지 잠시 실행을 멈춘다. 여기서 꼭 다 멈추는 것이 아니다.

 

Execution Barrier, (모든)이 아닐 수 있다.

 

메모리 베리어, 그냥 알아서 해주지..C양..

 

메모리 베리어가 없다면 Thread E, F, G는 Thread A, C, D에서 쓴 데이터가 아닌 엉뚱한(기 존에 있던 쓸 모없거나 잘 못된 값)을 가지고와서 작업을 해버리는 상황이 발생한다.

혹시나 "이 넘의 Cache를 왜 써서 이런 걸뱅이같은 상황을 만드나요?"라고 질문하면.. "안 그려면 겁나 느려요"라고 대답해 주겠다.

 

번외 : 또다른 베리어(ComputeShader과 상관없이)

Barrier는 단순히 "막아주는 벽"이라는 뜻이라 이 곳 저 곳에 그냥 막 붙여 쓰기만 하면 되는 단어다. 위의 2가지 말고도 이 걸뱅이같은 프로그래밍에는 또 다른 Barrier가 있다.(지금 설명할 것 말고도 이미 더 있을 수 있고, 아마 또 더 만들어 질 거다. 이런 주인장!)

Instruction Barrier

다음의 코드를 보자. 뭐 CPU에서 동작하는 코드라고 치자.

line 1 : int a = 1;
line 2 : int b = 1;

이 코드에 실제 "line1이 먼저 실행 될까? 아니면 line2가 먼저 실행이 될까?" 답은 "몰라요"다. CPU를 만드는 칭구들이 대ㄱㅏㄹ빡을 열심히 굴려서 속도가 떨어지지 않게 하려고 재간을 부렸는 데, 이 코드의 특성상 line1과 line2는 어떤 것이 먼저 실행되든 상관이 없다. 따라서 필요에 따라 실행 순서를 CPU자체에서 뒤바꿔 버리기도 한다. 심지어 동시에 실행되기도 한다. 이런 상황에서 "아가야~ 이 거 순서대로 실행해라~"라는 Instruction Barrier가 있다.

line 1 : int a = 1;
        (Instruction Barrier)
line 2 : int b = 1;

이렇게 넣어줘야 line1이 먼저 수행되고 line2가 다음에 수행된다.

 

CPU뿐 만아니라 C-Compiler도 최적화한다고 순서를 바꾸기도 한다. 이런 것들은 Instruction Barrier라고 한다.(필자[갓댕치]가 그냥 정한 말이다.)

혹시 그냥 "프로그래밍에서 Barrier가 뭐지?"하고 찾아온 사람을 위해 적어 놨다.

 

 

 

다시 ComputeShader의 베리어들

Execution barrier와 memory barrier의 개념을 잡았으니, 실제하려고 했던 ComputeShader로 돌아와 보자. ComputeShader에는 크게 두가지의 barrier가 있다. 한개는 GL명령들 사이에서 즉, API에서 사용하는 Barrier가 있고(API BARRIER), Dispatch를 하면 이 Dispatch내부에서 쓰는 barrier(shader barrier)가 있다.

API Barrier는 메모리 베리어만 있고, Shader Barrier는 execution barrier와 memory barrier 둘 다 있다.

 

API Barrier

다음의 예제를 보자..

출처 : https://arm-software.github.io/opengl-es-sdk-for-android/compute_intro.html

glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buffer);
glUseProgram(update_vbo_program);
glDispatchCompute(NUM_WORK_GROUPS, 1, 1); // <-- Write to buffer

// Ensure that writes to buffer are visible to subsequent GL commands which try to read from it.
// Essentially, it tells the GPU to flush and/or invalidate its appropriate caches.
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT);

glUseProgram(render_program);
glBindVertexArray(vertex_array);
glDrawElements(GL_TRIANGLES, ...); // <-- Read from buffer

위 코드는 ComputeShader로 SSBO에 뭔가 연산작업을 하고, 그 SSBO를 Vertex Attribute Array로 사용하려는 코드이다.

glMemoryBarrier 윗 부분과 아래 부분으로 나뉘게 된다.

위 부분의 glDispatchCompute호출이 API상에서 완료되면[사실상 즉시 완료된다] GPU는 ComputeShader를 돌려 연산을 하고 SSBO에 결과를 작성하고 있는 중이다. GPU에서 작업이 끝났다고 할 수 없다. glMemoryBarrier() 함수 없이 위의 코드를 실행한다면, 아래부분에서 그리기 작업을 할 때, 아직 완료되지 않은 결과를 가지고 그리기 작업을 해버리게 된다. 이는 Cache Flush가 완료되기 이전에 그리기 작업이 시작되기 때문이다. 따라서 Cache Flush작업을 하라는 함수가 glMemoryBarrier()함수고 Cach Flush가 끝나야 그리기 작업이 시작되는 것이다. 함수이름 그대로 이 함수는 Execution barrier가 아니고 Memory barrier다.

 

이 glMemoryBarrier는 좀 특이한 방법으로 사용해야 한다.

위의 예제에서 파라미터로 GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT를 사용했는 데, 이 사용법이 올바른 것이다.

GL_BUFFER_UPDATE_BARRIER_BIT를 사용하는 것이 맞다고 생각할 수 있는 데, 이 함수는 "이전에 한 작업을 완료하라" 가 아니고, 이후에 이 작업을 할 것이니, "이 작업을 할 수 있게 Cache를 Flush하라"라는 뜻이다.

위 예제에서는 vertex arrtibute array로 사용할 것이니, GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT가 맞는 것이다.

 

glMemoryBarrier -> http://docs.gl/es3/glMemoryBarrier 여기서 나머지 값들을 전부 확인하길 바란다.

ComputeShader르 처음 공부하는 사람이라면 꼭 자세히 봐야 한다.

 

 

 

 

 

 

 

Replies
Reply Write