아래처럼 다양한 형식의 2차원 백터에 대해 덧셈 연산을 하기 위한 아래 함수들이 준비되어 있었는데, 매번 자료형에 맞춰서 함수명을 바꿔가며 코드를 적는게 뭐랄까... 덜 세련되어 보여 vectormath::add()
라는 함수 하나로 오버로딩하려 마음먹은 적이 최근 있었다.
이 과정에서 있었던 동료와의 논의가 나름 재미있어서 옮겨봄.
namespace vectormath {
template <typename uv1, typename uv2>
constexpr auto uvAdd(const uv1& vec1, const uv2& vec2) noexcept
return uv1(vec1.u + vec2.u, vec1.v + vec2.v);
}
template <typename xy1, typename xy2>
constexpr auto xyAdd(const xy1& vec1, const xy2& vec2) noexcept {
return xy1(vec1.x + vec2.x, vec1.y + vec2.y);
}
template <typename XY1, typename XY2>
constexpr auto XYAdd(const XY1& vec1, const XY2& vec2) noexcept {
return XY1(vec1.X + vec2.X, vec1.Y + vec2.Y);
}
template <typename Component>
constexpr auto vec2Add(const Component vec0[2], const Component vec1[2], Component vec2[2]) noexcept {
vec2[0] = vec0[0] + vec1[0];
vec2[1] = vec0[1] + vec1[1];
}
}
concept
)처음엔 SFINAE를 사용하면 어찌어찌 되지 않을까... 라는 생각을 가졌다. 멤버 함수에 대해서만이라면 아래처럼 적용하면 되니까.
template <typename xy> requires std::is_fundamental_v<decltype(xy::x)>
xy add(const xy& p0, const xy& p1) noexcept {
return xy(p0.x + p1.x, p0.y + p1.y);
}
template <typename XY> requires std::is_fundamental_v<decltype(XY::X)>
XY add(const XY& p0, const XY& p1) noexcept {
return xy(p0.X + p1.X, p0.Y + p1.Y);
}
template <typename uv> requires std::is_fundamental_v<decltype(uv::u)>
uv add(const uv& p0, const uv& p1) noexcept {
return uv(p0.u + p1.u, p0.v + p1.v);
}
std::index_sequence
여기까진 쉬웠고, 배열 형태의 데이터에 대해 어떻게 하면 쉽게 확장할까... 하고 있던 차에 동료가 PR 커멘트로 std::index_sequence
를 사용하는 방법을 제안해주었다.
template <size_t Dimension, typename Component, size_t... AxesIndices>
void vecAdd_impl(const Component vec0[Dimension], const Component vec1[Dimension],
Component vec2[Dimension], std::index_sequence<AxesIndices...>) noexcept {
for (size_t axisIndex : { AxesIndices... }) {
vec2[axisIndex] = vec0[axisIndex] + vec1[axisIndex];
}
}
template <size_t Dimension, typename Component>
void add(const Component vec0[Dimension], const Component vec1[Dimension],
Component vec2[Dimension]) noexcept {
vecAdd_impl<Dimension>(vec0, vec1, vec2, std::make_index_sequence<Dimension>());
}
int main() {
double vec2[3][2] = {
{ 1, 1 },
{ 2, 2 },
{ 0, 0 } };
double vec3[3][3] = {
{ 1, 1, 1 },
{ 2, 2, 2 },
{ 0, 0, 0 } };
add<2>(vec2[0], vec2[1], vec2[2]);
add<3>(vec3[0], vec3[1], vec3[2]);
return 0;
}
모르던 초식이라 신박하긴 한데... 왠지 이런 초 간단한 함수에 range-based for loop를, 그것도 braced-init-list 까지 사용하면 왠지 unroll되지 않을 것도 같다는 염려에 아래처럼 (소싯적에 안드레이 알렉산드레스쿠 형님이 Modern C++ Design에서 날 피토하게 하셨던) template recursion을 적용해보면 어떻게냐고 했고...
template <size_t Dimension, typename Component>
struct addImpl {
addImpl(const Component* vec0, const Component* vec1, Component* vec2) noexcept {
vec2[Dimension] = vec0[Dimension] + vec1[Dimension];
addImpl<Dimension - 1, Component>(vec0, vec1, vec2);
}
};
template <typename Component>
struct addImpl<0, Component> {
addImpl(const Component* vec0, const Component* vec1, Component* vec2) noexcept {
vec2[0] = vec0[0] + vec1[0];
}
};
template <size_t Dimension, typename Component>
void add(const Component* vec0, const Component* vec1, Component* vec2) noexcept {
addImpl<Dimension - 1, Component>(vec0, vec1, vec2);
}
비록 컴파일 타임이지만 재귀호출이 마음에 안들어 이쯤 되면 그냥 아래처럼 풀어서 하는 건 어떠냐고 재차 제안했었다.
template <size_t Dimension, typename Component>
struct addImpl {};
template <typename Component>
struct addImpl<3, Component> {
addImpl(const Component* vec0, const Component* vec1, Component* vec2) noexcept {
vec2[0] = vec0[0] + vec1[0];
vec2[1] = vec0[1] + vec1[1];
vec2[2] = vec0[2] + vec1[2];
}
};
template <typename Component>
struct addImpl<2, Component> {
addImpl(const Component* vec0, const Component* vec1, Component* vec2) noexcept {
vec2[0] = vec0[0] + vec1[0];
vec2[1] = vec0[1] + vec1[1];
}
};
if constexpr
그 동료분이 다시 제안한 마지막 방법이 가장 나이스하더라... 이것 역시 신박하게도 if constexpr
을 사용하는 방법. 요즘 나오는 책 좀 사서 문법 공부를 다시해야겠다는 생각이 든다...ㅎㅎ
template <size_t Dimension, typename Component>
void add(const Component* vec0, const Component* vec1, Component* vec2) noexcept {
if constexpr (Dimension == 0) {
vec2[0] = vec0[0] + vec1[0];
} else {
vec2[Dimension - 1] = vec0[Dimension - 1] + vec1[Dimension - 1];
add<Dimension - 1, Component>(vec0, vec1, vec2);
}
}
결과적으로 SFINAE로 구현한 세 함수와, 마지만 if constexpr
버전을 모아 아래처럼 add
함수 하나로 오버로딩이 가능해졌다. (마지막 네 줄)
template <typename Component>
struct Point2 {
Coordinate x;
Coordinate y;
};
template <typename Component>
struct POINT2 {
Coordinate X;
Coordinate X;
};
template <typename Component>
struct TexCoord2 {
Coordinate u;
Coordinate v;
};
void myfunc() {
Point2<float> p0(1, 2), p1(3, 4);
POINT2<int64_t> P0(1, 2), P1(3, 4);
TexCoord2<double> uv0(0.1, 0.2), uv1(0.3, 0.4);
int vec0[2] = { 1, 2 }, vec1[2] = { 3, 4 }, vec[2];
auto p = vectormath::add(p0, p1);
auto P = vectormath::add(P0, P1);
auto uv = vectormath::add(uv0, uv1);
vectormath::add<2>(vec0, vec1, vec);
}
마지막으로 위에 나열했던 함수들이 정말 loop unroll이 되는지를 확인해봤다. 아래 예제처럼 동일한 목적을 수행하는 add0
, add1
, add2
, add3
네 개의 함수를 작성해서 disassembled code를 살펴보는 방식으로.
(어셈블리는 까막눈이지만 뭐 눈치껏...)
template <typename Component>
void add0(size_t n, const Component* vec0, const Component* vec1, Component* vec2) noexcept {
for (size_t i = 0; i != n; ++i) {
vec2[i] = vec0[i] + vec1[i];
}
}
template <size_t dimension, typename Component, size_t... axesIndices>
void add1_impl(const Component vec0[dimension], const Component vec1[dimension], Component vec2[dimension], std::index_sequence<axesIndices...>) noexcept {
for (size_t axisIndex : { axesIndices... }) {
vec2[axisIndex] = vec0[axisIndex] + vec1[axisIndex];
}
}
template <size_t dimension, typename Component>
void add1(const Component vec0[dimension], const Component vec1[dimension], Component vec2[dimension]) noexcept {
add1_impl<dimension>(vec0, vec1, vec2, std::make_index_sequence<dimension>());
}
template <size_t Dimension, typename Component>
void add2(const Component* vec0, const Component* vec1, Component* vec2) noexcept {
if constexpr (Dimension == 1) {
vec2[0] = vec0[0] + vec1[0];
} else {
vec2[Dimension - 1] = vec0[Dimension - 1] + vec1[Dimension - 1];
add2<Dimension - 1, Component>(vec0, vec1, vec2);
}
}
template <size_t Dimension, typename Component>
void add3(const Component vec0[3], const Component vec1[3], Component vec2[3]) noexcept {
vec2[0] = vec0[0] + vec1[0];
vec2[1] = vec0[1] + vec1[1];
vec2[2] = vec0[2] + vec1[2];
}
아래는 add0()
. 의도적으로 unroll 되지 않는 케이스를 만들어본 것이다. inlining을 억제하려 n
을 int n; sscanf("3", "%d", &n);
와 같은 방식으로 초기화한 것인데 했더니 코드가 많이 지저분하더라. (호출부로 돌아가는 것 포함 conditional jump가 두 개...)
add0(n, vec[0], vec[1], vec0);
00007FF62E935780 movups xmm1,xmmword ptr [rbp+rax*8+108h]
00007FF62E935788 movups xmm0,xmmword ptr [rbp+rax*8+0F0h]
00007FF62E935790 addpd xmm1,xmm0
00007FF62E935794 movups xmmword ptr [rbp+rax*8+0D8h],xmm1
00007FF62E93579C movups xmm2,xmmword ptr [rbp+rax*8+118h]
00007FF62E9357A4 movups xmm0,xmmword ptr [rbp+rax*8+100h]
00007FF62E9357AC addpd xmm2,xmm0
00007FF62E9357B0 movups xmmword ptr [rbp+rax*8+0E8h],xmm2
00007FF62E9357B8 movups xmm1,xmmword ptr [rbp+rax*8+128h]
00007FF62E9357C0 movups xmm0,xmmword ptr [rbp+rax*8+110h]
00007FF62E9357C8 addpd xmm1,xmm0
00007FF62E9357CC movups xmmword ptr [rbp+rax*8+0F8h],xmm1
00007FF62E9357D4 movups xmm2,xmmword ptr [rbp+rax*8+120h]
00007FF62E9357DC movups xmm0,xmmword ptr [rbp+rax*8+138h]
00007FF62E9357E4 addpd xmm2,xmm0
00007FF62E9357E8 movups xmmword ptr [rbp+rax*8+108h],xmm2
00007FF62E9357F0 add rax,8
00007FF62E9357F4 cmp rax,rdx
00007FF62E9357F7 jne TestBody+100h (07FF62E935780h)
00007FF62E9357F9 cmp rax,rcx
00007FF62E9357FC je TestBody+1A3h (07FF62E935823h)
참고로, add0()
에서 sscanf()
를 사용하지 않고 int n = 3;
으로 주면 아래처럼 루프가 unroll된다.
int n = 3;
add0(n, vec[0], vec[1], vec0);
00007FF7F6B45729 movsd xmm6,mmword ptr [__real@4022000000000000 (07FF7F6F52F90h)]
00007FF7F6B45731 movaps xmm3,xmm6
00007FF7F6B45734 movq r9,xmm6
00007FF7F6B45739 movsd xmm7,mmword ptr [__real@401c000000000000 (07FF7F6F52F70h)]
00007FF7F6B45741 movaps xmm2,xmm7
00007FF7F6B45744 movq r8,xmm7
00007FF7F6B45749 movsd xmm8,mmword ptr [__real@4014000000000000 (07FF7F6F52F50h)]
00007FF7F6B45752 movaps xmm1,xmm8
00007FF7F6B45756 movq rdx,xmm1
00007FF7F6B4575B lea rcx,[__xmm@00000000000000003fe0000000000000+898h (07FF7F7029B98h)]
다음은 std::index_sequence
를 사용한 add1()
이다. add0()
에 비해 코드는 간소화되었지만 완전히 unroll되지는 않아서 conditional jump가 존재한다.
add1<3>(vec[0], vec[1], vec1);
00007FF62E935856 mov qword ptr [rbp+0D8h],r14
00007FF62E93585D mov qword ptr [rbp+0E0h],1
00007FF62E935868 mov qword ptr [rbp+0E8h],2
00007FF62E935873 lea rcx,[rbp+0D8h]
00007FF62E93587A nop word ptr [rax+rax]
00007FF62E935880 mov rax,qword ptr [rcx]
00007FF62E935883 movsd xmm0,mmword ptr [rbp+rax*8+108h]
00007FF62E93588C addsd xmm0,mmword ptr [rbp+rax*8+0F0h]
00007FF62E935895 movsd mmword ptr [rbp+rax*8+1E0h],xmm0
00007FF62E93589E add rcx,8
00007FF62E9358A2 lea rax,[rbp+0F0h]
00007FF62E9358A9 cmp rcx,rax
00007FF62E9358AC jne TestBody+200h (07FF62E935880h)
if constexpr
을 사용한 add2()
와 직접 루프를 풀어서 적은 add3()
는 사실상 동일한 코드를 생성한다. 재미있는 것은 add2()
의 경우 recursion 과정에서 생성되는 코드와 disassembled code가 역순이라는 것이다. 컴파일러가 최적화 과정에서 memory ordering (instruction reordering)을 적용했는가보다...
add2<3>(vec[0], vec[1], vec2);
00007FF62E9358E1 movsd xmm3,mmword ptr [__real@4022000000000000 (07FF62ED42F90h)]
00007FF62E9358E9 movq r9,xmm3
00007FF62E9358EE movsd xmm2,mmword ptr [__real@401c000000000000 (07FF62ED42F70h)]
00007FF62E9358F6 movq r8,xmm2
00007FF62E9358FB movsd xmm1,mmword ptr [__real@4014000000000000 (07FF62ED42F50h)]
00007FF62E935903 movq rdx,xmm1
00007FF62E935908 lea rcx,[__xmm@00000000000000003fe0000000000000+910h (07FF62EE19C10h)]
add3<3>(vec[0], vec[1], vec3);
00007FF62E935914 movsd xmm3,mmword ptr [__real@4022000000000000 (07FF62ED42F90h)]
00007FF62E93591C movq r9,xmm3
00007FF62E935921 movsd xmm2,mmword ptr [__real@401c000000000000 (07FF62ED42F70h)]
00007FF62E935929 movq r8,xmm2
00007FF62E93592E movsd xmm1,mmword ptr [__real@4014000000000000 (07FF62ED42F50h)]
00007FF62E935936 movq rdx,xmm1
00007FF62E93593B lea rcx,[__xmm@00000000000000003fe0000000000000+928h (07FF62EE19C28h)]
결론, 최적화 수준과 사용성 면에서 if constexpr
을 사용한 마지막 버전 add2
가 가장 낫더라...
template <size_t Dimension, typename Component>
void add2(const Component* vec0, const Component* vec1, Component* vec2) {
if constexpr (Dimension == 1) {
vec2[0] = vec0[0] + vec1[0];
} else {
vec2[Dimension - 1] = vec0[Dimension - 1] + vec1[Dimension - 1];
add2<Dimension - 1, Component>(vec0, vec1, vec2);
}
}