[C++] AVX2 벡터확장.
AVX2 벡터확장 (프로그램 속도최적화).
서론
오늘은 프로그램의 속도최적화와 관련된 이야기를 해볼까 한다.
앞선 포스팅에서 OpenMP 관련 내용도 게시했는데, 멀티스레드와는 결이 다른 방식이다.
요즘은 중국 우한에서 TIANMA 라는 국영기업 공장으로 출근하고 있다.
해당 공장에서 Open Metal Mask 라고 불리는 제품의 외관검사 프로젝트를 진행하고 있다.
제품의 크기가 크고 라인스캔 카메라로 초당 많은 양의 이미지가 들어오기 때문에,
실시간 검사속도가 아주 중요한 프로젝트이다.
따라서 불량검출을 위한 이미지 전처리 속도도 아주 중요했는데…
이와 관련해 어떤 방식으로 속도를 높였는지 얘기해볼까 한다.
AVX2 벡터확장이란?
bit depth 8 의 흑백이미지에서 영상처리를 할 때 32번의 연산을 해야할 일이 생겼다면
일반적인 CPU는 32 회 연산을 수행 할 것이다.
하지만 AVX2 벡터확장을 이용하면 CPU는 32byte 크기의 레지스터를 활용해 연산을 수행한다.
따라서 1회만 연산을 수행한다.
적용 방법
프로젝트 설정 > 구성 속성 > C/C++ > 코드 생성 > 고급 명령 집합 사용 > 고급 벡터 확장 2(/arch:AVX2)
프로젝트 설정 > 구성 속성 > C/C++ > 전처리기 > 전처리기 정의 > “__AVX2__” 추가
사용 예시
원본 이미지를 이진화하는 AVX2 사용 예시코드이다.
cv::Mat VisionProcess::CInspectAvi::Binarize(const cv::Mat& src, int dist, int threshold, int minCount, bool findDarker, int blurSize) {
// ── 전처리 (블러) ─────────────────────────────────────────
static thread_local cv::Mat processed;
if (blurSize > 0) {
cv::GaussianBlur(src, processed, cv::Size(blurSize, blurSize), 0);
}
else {
processed = src.clone();
}
// ── 기본 정보 ─────────────────────────────────────────────
const int rows = processed.rows;
const int cols = processed.cols;
const size_t step = processed.step;
cv::Mat dst(rows, cols, CV_8UC1, cv::Scalar(0));
const uchar* pBase = processed.data;
uchar* pDstBase = dst.data;
const size_t dstStep = dst.step;
// ── 8방향 1D 메모리 오프셋 (순서: 상·하·좌·우·좌상·우상·좌하·우하) ──
const long offsets[8] = {
-static_cast<long>(step * dist), // 상
static_cast<long>(step * dist), // 하
-static_cast<long>(dist), // 좌
static_cast<long>(dist), // 우
-static_cast<long>(step * dist) - dist, // 좌상
-static_cast<long>(step * dist) + dist, // 우상
static_cast<long>(step * dist) - dist, // 좌하
static_cast<long>(step * dist) + dist, // 우하
};
// ── x방향 오프셋 (스칼라 경계 체크용) ────────────────────
const int xOffsets[8] = {
0, 0, -dist, dist, -dist, dist, -dist, dist
};
// ── 타일 크기: L2 캐시(512KB) 기준 동적 계산 ─────────────
const int TILE_ROWS = std::max(4, std::min(64, (512 * 1024) / std::max(1, cols)));
const int tileCount = (rows - 2 * dist + TILE_ROWS - 1) / TILE_ROWS;
for (int t = 0; t < tileCount; ++t) {
#ifdef __AVX2__
const __m256i vThr = _mm256_set1_epi8(static_cast<char>(std::min(threshold, 255)));
const __m256i vMinCount = _mm256_set1_epi8(static_cast<char>(minCount - 1));
const __m256i vSign = _mm256_set1_epi8(static_cast<char>(0x80));
const __m256i vOne = _mm256_set1_epi8(1);
const int avxEnd = cols - 2 * dist - 32;
#endif
const int yStart = dist + t * TILE_ROWS;
const int yEnd = std::min(yStart + TILE_ROWS, rows - dist);
for (int y = yStart; y < yEnd; ++y) {
const uchar* pSrc = pBase + y * step;
uchar* pDst = pDstBase + y * dstStep;
int x = dist;
#ifdef __AVX2__
if (avxEnd >= dist) {
for (; x <= avxEnd; x += 32) {
const __m256i vCenter = _mm256_loadu_si256((const __m256i*)(pSrc + x));
__m256i vMatch = _mm256_setzero_si256();
for (int i = 0; i < 8; ++i) {
const __m256i vNeighbor = _mm256_loadu_si256((const __m256i*)(pSrc + x + offsets[i]));
const __m256i vDiff = findDarker
? _mm256_subs_epu8(vNeighbor, vCenter)
: _mm256_subs_epu8(vCenter, vNeighbor);
const __m256i vCond = _mm256_cmpgt_epi8(
_mm256_xor_si256(vDiff, vSign),
_mm256_xor_si256(vThr, vSign));
vMatch = _mm256_add_epi8(vMatch, _mm256_and_si256(vCond, vOne));
}
const __m256i vResult = _mm256_cmpgt_epi8(
_mm256_xor_si256(vMatch, vSign),
_mm256_xor_si256(vMinCount, vSign));
_mm256_storeu_si256((__m256i*)(pDst + x), vResult);
}
}
#endif
for (; x < cols - dist; ++x) {
const uchar center = pSrc[x];
int matchCount = 0;
for (int i = 0; i < 8; ++i) {
const int nx = x + xOffsets[i];
if (nx < dist || nx >= cols - dist) continue;
const uchar neighbor = *(pSrc + x + offsets[i]);
const int diff = findDarker
? std::max(0, static_cast<int>(neighbor) - static_cast<int>(center))
: std::max(0, static_cast<int>(center) - static_cast<int>(neighbor));
if (diff > static_cast<uchar>(threshold) && ++matchCount >= minCount) {
pDst[x] = 255;
break;
}
}
}
}
}
return dst;
}
AVX2 전처리 정의 외부에 유사한 코드가 작성되어 있는 것을 볼 수 있다.
주요 차이점은 x의 값이 32개씩 증가하는지 1씩 증가하는지 이다.
AVX2는 32개의 픽셀씩 작업을 수행하기 때문에 총 작업량이 32의 배수와 다르다면, AVX2가 처리할 수 없는 영역이므로 예외처리가 필요한 것이다.
이를 벡터영역과 스칼라영역이라고 명칭한다.
AVX2는 알겠는데 AVX1은 무엇이었을까??
| 이름 | AVX | AVX2 |
|---|---|---|
| 출시 | 2011 | 2013 |
| 레지스터 크기 (Bit) | 256 | 256 |
| 주력 연산 대상 | Float, Double | ALL |
| 정수 처리량 | 128 | 256 |
이 외에도 FMA3(고속 곱셈-누산)을 지원하는지, Gather(데이터 섞기)를 지원하는지 등의 차이도 있지만 지금 내가 잘 모르는 내용이기도 하고 위 예시 코드에서 사용되지도 않았으니 생략하겠다.
나와 같은 상황, 큰 이미지에서 단순 비교를 통해 이미지 이진화를 해야하는 상황에선 AVX2 가 더 올바른 선택이었고. 굳이 그렇지 않더라도 상위호환이기 때문에 그냥 AVX2 를 쓰면 된다는 뜻이다.
다만 사용하는 CPU가 2013년 이전에 출시했다면 AVX1 으로 타협해야 할 것이다.
결론
AVX2 (벡터확장 가속) 기술은 CPU 코어의 효율을 높여주는 것이기 때문에 병렬처리, 멀티스레드와는 다른 개념이다. 둘 다 사용하면 좋다.
끝.
댓글남기기