반응형

http://studycan.tistory.com/96

[TBB] 왜 스레딩 빌딩 블록인가?

인텔 스레딩 빌딩 블록(Intel Threading Building Blocks)은 C++로 병렬 프로그램을 구현할 때 사용할 수 있는 일종의 라이브러리이며, 풍부하고 완벽한 방법론을 제공함으로써 스레드 처리에 대한 전문 지식 없이도 멀티코어 프로세서 시스템의 성능을 향상시킬 수 있도록 도와준다. 스레딩 빌딩 블록은 단순히 기존의 스레드를 대체하는 것이 아니라, 플랫폼의 세부 사항과 스레드 처리 메커니즘을 추상화한 ‘태스크’ 개념을 기반으로 하는 높은 수준의 병렬처리 기술이라고 할 수 있으며 성능(performance)과 조정성(scalability)를 목표로 한다.

이 글에서는 인텔 스레딩 빌딩 블록에 대해 소개하고 C++에서 사용되고 있는 기존의 유사 기술들과 비교하여 더 나은 점을 무엇인지 알아본다. 스레딩 빌딩 블록은 C++의 템플릿과 제네릭 프로그래밍 개념에 기반을 두고 있지만 이 글을 읽기 위해 이 개념들과 스레드 처리에 대한 사전 경험은 전혀 필요하지 않다.

설치 관련
http://studycan.tistory.com/94

개요

위 그림은 2009년 12월 19일 기준으로 다나와 사이트에서 판매되고 있는 CPU 중 1위 부터 10위까지 판매 순위이이다. 판매량을 보면 알다시피 듀얼코어나 쿼드코어 등의 멀티코어 프로세서가 대중화되고 있지만, 기존 스레딩 패키지(threading packages)를 사용하여 간단한 병렬 반복문을 작성하는 것 조차 아직 지루한 작업이다. 프로세서 코어의 개수가 증가함에 따라 프로그램의 성능도 효율적으로 조정되도록 작성하는 것은 이 작업보다 훨씬 어렵다. 여기서, 프로세서 코어의 개수가 증가할 때 프로그램의 성능도 그에 적응하여 향상된다는 의미를 가진 용어를 ‘조정성(Scalability)’이라고 정의해보자.

스레딩 빌딩 블록은 표준 C++ 코드로 조정성 있는 병렬 프로그램을 작성해주는 라이브러리이다. C++버전이나 컴파일러가 필요하지 않으며, 프로세서 종류나 운영체제와 관계없이 사실상 모든 C++ 컴파일러에 사용할 수 있다는 것이 큰 장점이다.

스레딩 빌딩 블록은 공통적 병렬 반복 패턴(parallel iteration patterns)을 위한 템플릿을 제공하는데, 이 템플릿들을 사용하면 프로그래머는 동기화(synchronization), 부하 분산(load balancing), 캐시 최적화(cache optimization)와 같은 전문 기술을 알지 못하더라도 여러 개의 프로세서 코어로부터 단 하나의 프로세서 코어에서도 속도 증가 효과를 거둘 수 있다. 스레딩 빌딩 블록 라이브러리를 사용하기 위해 스레드(thread)가 아닌 ‘태스크(task)’를 규정하고, 이 라이브러리를 하여금 이 태스크들을 스레드에 효율적인 방식으로 대응시키게 한다. 이렇게 하면 더욱 편리한 병렬처리를 구현할 수 있고, 스레드를 직접 사용하는 것보다 더 나은 결과를 얻을 수 있다.

어떤 이점이 있는가?

오늘 날의 컴퓨팅 환경에서, 프로그래머의 목표는 ‘조정성’이다. 즉 듀얼코어 프록세서에서 두 개의 코어가 모두 사용되고, 쿼드코어 프로세서에서는 네 개의 코어가 모두 사용되는 것이다. 그 이상의 코어를 가진 프로세서에서도 마찬가지다.

다양한 병렬 프로그래밍을 위한 방법들 중에서 TBB 는 다음과 같은 이유들로 기존 스레딩 패키지와 차별된다.

스레드 대신 ‘태스크’를 규정한다.
대부분의 스레딩 패키지들은 스레드를 일일히 생성, 연결 및 관리 등을 하는 하드웨어에 가까운 낮은 수준의 무거운 구조이기 때문에 비효율적이고, 스레드를 직접 사용하여 프로그래밍 할 때에는 일일이 스레드와 태스크를 효율적으로 대응시켜야 하는 불편함이 있는데 TBB 라이브러리는 프로세서의 리소스를 효율적으로 사용하는 방식으로 태스크를 스레드에 자동 스케줄링해 준다. 이 런타임은 우리가 규정할 많은 태스크들의 부하를 분산시키는데 매우 효과적이다. 이외에도 원초적 스레드를 직접 사용하는 방법으로 어셈블리 언어를 사용하는 것인데 이는 그 비용이 엄청나기 때문에 좋은 방안이 아니다.

성능 향상을 위한 스레드 처리 기술을 지향한다.
다른 스레딩 패키지들은 궁극적인 해결책이 아닌, 기반 기술만 제공하는 낮은 수준의 도구가 되는 경향이 있다면 TBB는 계산의 비중이 높은 작업을 병렬로 처리하고 더 높은 수준을 가지면서 더 단순한 해결책을 제공하는데 목표를 두고 처리

다른 스레딩 패키지들과도 잘 호환된다.
OpenMP등과 같은 다른 스레딩 패키지들과도 호환성을 가진다. 내부적으로 OpenMP를 사용하는 IPP나 MKL등 라이브러리를 자유롭게 연결할 수 있다.

조정성이 있는 데이터 병렬 프로그래밍을 강력히 지향한다.
일반적으로 기능 블록의 개수가 고정되어 있는 등의 문제로 하나의 프로그램을 여러 개의 기능 블록을 나누고 각 블록을 나누고 각 블록에 개별 스레드를 할당하는 방식으로는 높은 조정성을 기대하기 힘들다. TBB는 데이터 병렬 프로그래밍을 강력히 지향하는데 이 방식은 데이터 집합을 더 작은 조각으로 나누기 때문에 프로세서를 추가함에 따라 프로그램 성능이 향상된다. 전역 태스크 큐(global task queue)와 같은 고전적 병목 현상들도 막아주는데, 전역 태스크 큐에서 각 프로세서는 새 태스크를 얻기 위해 기다리면서 잠금 상태가 되어야 한다.

제네릭 프로그래밍을 기반으로 한다.
TBB는 제네릭 프로그래밍 방식을 사용하는데 제네릭의 본질은 최소의 제약을 가진 가장 일반적인 알고리즘을 작성하는 데 있다.

원초적 스레드 처리 방식과 MPI의 비교

POSIX 스레드(pthreads)나 윈도우 스레드 같은 원초적 스레드 인터페이스를 사용하는 프로그래밍은 공유 메모리를 사용한 병렬 처리를 구현할 때 많은 프로그래머들이 사용하는 방식 중 하나였다. Boost Threads와 같이 이식성을 높여주는 래퍼(wrapper)들도 있는데, 이 것들은 이식성이 매우 좋은 원초적 스레드 인터페이스이다. 수천 개의 프로세서를 탑재한 슈퍼컴퓨터에서는 일반적으로 공유 메로리를 사용하는 방식이 없기 때문에, 그 유명한 MPI(Message Passing Interface) 표준을 통해 메시지 전달 기법을 사용하여 병렬처리를 수행한다.

원초적 스레드와 MPI는 가장 낮은 수준에서 병렬처리를 구현하다. 따라서 병럴 처리에 대한 어셈블리 언어라고도 말할 수 있다. 이런 방식들은 최대의 유연성을 주지만, 프로그래머의 노력, 디버깅 시간 및 유지보수 면에서 많은 비용이 든다는 단점이 있다.

원초적 스레드를 사용할 경우 알게 될 사실은 정확하고 효율적인 구현을 위한 기본 작업과 데이터 공유 처리가 어렵고 지루하다는 것이다. 이 때 작성되는 코드는 운영체제의 특정 처리 기능에 종속적이다. 그리고 너무 낮은 수준 접근이기 때문에 직관적이지 못하며, 결과적으로 그 코드를 조정 가능한 성능을 갖도록 설계하기 힘들다.

논리적 스레드 대신 태스크를 사용할 경우, 얻을 수 있는 또 다른 이점은 태스크가 훨씬 더 가볍다는 것이다. 리눅스 시스템에서, 태스크를 시작하고 종료시키는 시간은 스레드를 시작하고 종료하는 것보다 18배 정도 빠르다. 윈도우 시스템에서는 100배가 넘는다.

스레드와 MPI를 사용하면, 결국 태스크들을 프로세서 코어들에 명시적으로 대응시켜 주어야 한다. 태스크를 사용한 병렬 처리 구현하기 위해 스레딩 빌딩 블록을 사용한다면 스레드를 사용할 때 보다 더 정교하게 동시성(concurrency)을 구현할 수 있으며, 그로 인해 조정성(scalability)이 향상 된다.

OpenMP와의 비교

TBB와 함께, 또 하나의 유망한 스레드 추상화 기술은 바로 OpenMP이다. 오늘날 가장 성공적인 병렬 확장 기술인 OpenMP는 포트란이나 C프로그램에서 사용할 수 있으며 프라그마(pragma), 루틴, 환경 변수들로 구성된 언어 확장팩이다. OpenMP는 사용자 병렬 프로그램을 구현하게 해주며, 컴파일러로 하여금 프로그래머의 의도를 반영한 프로그램을 생성하게 해준다. 이 기술은 포트란과 C언어의 한계를 극복한 중대한 발전이고, 컴파일러가 코드에서 병렬처리할 부분을 자동으로 찾아내지 못하게 막아준다.

C프로그램에 대해 고려해본다면 OpenMP는 ‘C로 작성된 포트란 스타일 코드에 탁월하다’라고 알려져 왔다. OpenMP는 루프 구조와 C코드에 초점을 맞추고 있기 때문에 이것은 틀린 말이 아니다. C++라고 해서 OpenMP가 특별한 것을 제공하는 것은 아니다. OpenMP의 구조는 백터 처리용 슈퍼 컴퓨터를 위해 개발된 루프 중첩 구조와 동일하다. 당시 이 방식의 슈퍼컴퓨터에는 초기 세대 병렬 프로세서들로 탑재되어 있었으며, 그 안에서 실행되는 프로그램은 상당한 양의 포트란 코드로 작성되었고, 겹겹이 중첩된 루프들 안에서 엄청난 양의 계산 작업을 수행되었다. 그리고 그러한 중첩 루프들을 병렬 코드로 변환 하는 것은 결과 면에서 매우 가치 있는 작업이 될 수 있었다.

OpenMP는 세 가지 스케줄링 방식(static, guided 및 dynamic) 중 하나를 선택해야 한다. 그러나 TBB를 사용하면 프로그래머는 이런 스케줄링 정책에 대해 전혀 걱정할 필요 없다. 자동으로 ‘분할 정복(dvivide-and-conquer)’이라는 한 가지 스케줄링 방식만을 사용함으로써 이러한 문제를 해결했다. 또한 워크 스틸링(work stealing, 로딩된 프로세서에서 유휴 상태에 있는 프로세서로 태스크를 이동시키는 테크닉) 기능도 구현되어 있는데, 이 기능은 dynamic 및 guided 스케줄링과 비교되지만 중앙 처리자 없이도 수행된다는 점이 다르다. 때때로 다른 프로세서나 또는 동시적으로 연관된 코드에 방해 받지 않는 시스템에서는 static 스케줄링이 더 빠르기도 하다. 하지만 중첩된 병렬 처리 환경에서는 분할 정복 방식이 잘 맞는다.

TBB에서 제네릭 프로그래밍을 지원한다는 것은 병렬처리의 대상이 내장된 기본 형식들에 국한되지 않는다는 것을 의미한다. OpenMP는 기본 형식에 대해서만 리덕션(reduction)을 허용하는 반면, TBB의 parallel_reduce는 모든 형식에서 사용할 수 있다.

TBB는 OpenMP의 약점을 극복하고자 C++를 지원하도록 설계되었고, C++ 프로그램에 대해 가장 간단한 해결책을 제공해준다. TBB는 정적 루프 중첩 구조에 국한되지 않고, 그 이상으로 태스크 기반의 병렬처리를 위한 재귀 모델과 제네릭 알고리즘을 구현한다.

재귀적 분할, 태스크 스틸링 및 알고리즘

TBB의 병렬처리 모델을 직관적으로 만들기 위해 여러 가지 개념들이 사용되었다. 대부분의 개념들은 적절한 수준의 병렬 태스크가 될 때까지 문제를 필요한 만큼 재귀적으로 분할하는 방식을 사용한다. 걸격 이 방식은 작업을 더욱 명백하게 정적(static)으로 분할하는 것보다 훨씬 낫다고 밝혀지는데, 전역 태스크 큐와 태스크 스틸링을 사용할 때에도 완벽하게 들어 맞는다. 이것은 태스크 큐와 같은 전역 리소스의 사용을 막아주는 중대한 설계 고려 사항인데, 전역 리소스는 조정성을 제한 할 수 있다.

여러분은 병렬처리에 적용할 알고리즘 구조(for 루프, while 루프, 파이프라인, 분할 정복 등)의 선택을 고민하는 과정에서, 결국 그 것들을 조합해야 한다는 것을 알게 될 것이다. 예를 들어 여러분의 프로그램에서 파이프라인들의 병렬 집합과 이 파이프라인들을 제어 하는 parallel_for 루프를 조합하여 사용해야 한다는 것을 알게 되었다면, 이것을 구현하는 것도 쉽다는 것을 알게 될 것이다. 뿐만 아니라, 재귀와 태스크 스틸링을 기초 설계에 포함시킴으로써 응용 프로그램을 더욱 효율적이고 조정성 있게 만들 수 있다.

반응형

+ Recent posts