6.159 Eigen 라이브러리와 로봇 소프트웨어 구현
1. Eigen 라이브러리의 개요
Eigen은 C++ 템플릿 기반의 선형대수 라이브러리로, 헤더 전용(header-only) 구조를 가지며 별도의 컴파일이나 링크가 필요하지 않다. 로봇공학 소프트웨어에서 가장 널리 사용되는 선형대수 라이브러리이며, ROS(Robot Operating System), Pinocchio, Drake, MoveIt 등 주요 로봇공학 프레임워크가 Eigen에 의존한다.
Eigen의 핵심 설계 철학은 다음과 같다.
- 표현 템플릿(expression template): 지연 평가(lazy evaluation)를 통해 불필요한 임시 객체 생성을 방지한다.
- 컴파일 타임 크기 특화: 고정 크기 행렬에 대해 컴파일 시점에 루프 전개와 SIMD 벡터화를 수행한다.
- BLAS/LAPACK 수준의 성능: 대규모 행렬에 대해 최적화된 BLAS 구현에 근접하는 성능을 제공한다.
2. 기본 행렬과 벡터 타입
2.1 고정 크기 타입
Eigen에서 컴파일 시점에 크기가 결정되는 고정 크기(fixed-size) 타입은 스택(stack)에 할당되며 동적 메모리 할당 오버헤드가 없다.
로봇공학에서 자주 사용되는 고정 크기 타입은 다음과 같다.
| 타입 | 의미 | 크기 |
|---|---|---|
Eigen::Vector3d | 3차원 배정밀도 벡터 | 3 \times 1 |
Eigen::Vector4d | 4차원 배정밀도 벡터 | 4 \times 1 |
Eigen::Matrix3d | 3 \times 3 배정밀도 행렬 | 3 \times 3 |
Eigen::Matrix4d | 4 \times 4 배정밀도 행렬 | 4 \times 4 |
Eigen::Quaterniond | 배정밀도 쿼터니언 | 4개 성분 |
Eigen::AngleAxisd | 축-각도 표현 | 축(3) + 각도(1) |
Eigen::Isometry3d | 강체 변환 (SE(3)) | 4 \times 4 |
일반적인 고정 크기 행렬은 Eigen::Matrix<Scalar, Rows, Cols> 템플릿으로 정의된다. 예를 들어, 6 \times n 자코비안 행렬의 고정 행 크기 버전은 다음과 같다.
\texttt{Eigen::Matrix<double, 6, Eigen::Dynamic>}
이 타입은 행 수가 6으로 고정되고 열 수는 실행 시점에 결정되는 반고정(semi-fixed) 크기 행렬이다.
동적 크기 타입
실행 시점에 크기가 결정되는 동적 크기(dynamic-size) 타입은 힙(heap)에 할당된다.
| 타입 | 의미 |
|---|---|
Eigen::VectorXd | 동적 크기 배정밀도 벡터 |
Eigen::MatrixXd | 동적 크기 배정밀도 행렬 |
Eigen::VectorXf | 동적 크기 단정밀도 벡터 |
Eigen::MatrixXf | 동적 크기 단정밀도 행렬 |
n자유도 로봇의 관절 변수 벡터 \mathbf{q} \in \mathbb{R}^n, 관성 행렬 \mathbf{M} \in \mathbb{R}^{n \times n} 등 자유도에 따라 크기가 달라지는 양은 동적 크기 타입을 사용한다.
고정 크기와 동적 크기의 성능 차이
고정 크기 행렬은 다음과 같은 이유로 소규모에서 동적 크기보다 훨씬 빠르다.
- 스택 할당으로
malloc/free호출이 없다. - 컴파일러가 루프를 완전히 전개(unroll)한다.
- SIMD 명령어가 직접 삽입된다.
4 \times 4 행렬 곱셈에서 고정 크기(Matrix4d)는 동적 크기(MatrixXd)에 비해 5~10배 빠를 수 있다. 로봇공학에서 동차 변환 행렬(4 \times 4)의 연쇄 곱은 항상 고정 크기 타입을 사용하여야 한다.
그러나 고정 크기 타입은 크기가 커지면(일반적으로 16 \times 16 이상) 스택 오버플로의 위험이 있으므로, 대규모 행렬은 동적 크기를 사용하여야 한다.
표현 템플릿과 지연 평가
Eigen의 핵심 최적화 기법인 표현 템플릿(expression template)은 C++ 템플릿 메타프로그래밍을 활용하여 수식의 연산 트리를 컴파일 시점에 구성한다.
예를 들어, \mathbf{v} = 2\mathbf{a} + 3\mathbf{b}의 계산에서 나이브 구현은 두 개의 임시 벡터를 생성한다.
\mathbf{t}_1 = 2\mathbf{a}, \quad \mathbf{t}_2 = 3\mathbf{b}, \quad \mathbf{v} = \mathbf{t}_1 + \mathbf{t}_2
이는 3번의 메모리 순회와 2개의 임시 할당을 필요로 한다. Eigen의 표현 템플릿은 이를 단일 루프로 융합(fuse)한다.
v_i = 2a_i + 3b_i, \quad i = 1, \ldots, n
이 최적화는 메모리 할당을 제거하고 메모리 순회를 1번으로 줄여, 메모리 대역폭 제한 연산에서 큰 성능 향상을 가져온다.
그러나 지연 평가가 항상 유리한 것은 아니다. 행렬-행렬 곱에서는 각 원소의 계산이 다른 원소의 결과에 의존하므로, Eigen은 GEMM 수준의 연산에서는 즉시 평가(eager evaluation)를 수행한다. .eval() 메서드를 명시적으로 호출하여 강제 평가를 할 수도 있다.
행렬 분해와 풀이
Eigen은 다양한 행렬 분해를 제공하며, 각 분해는 특정 행렬 유형과 용도에 최적화되어 있다.
선형 시스템 풀이를 위한 분해
| 분해 클래스 | 행렬 조건 | 속도 | 정확도 | 용도 |
|---|---|---|---|---|
LLT | SPD | 매우 빠름 | 좋음 | 관성 행렬 시스템 |
LDLT | 대칭 (부정치 가능) | 빠름 | 좋음 | KKT 시스템 |
PartialPivLU | 가역 | 빠름 | 보통 | 일반 시스템 |
FullPivLU | 일반 | 느림 | 매우 좋음 | 순위 판별 필요 시 |
HouseholderQR | 일반 | 중간 | 좋음 | 최소자승 (전순위) |
ColPivHouseholderQR | 일반 | 중간 | 매우 좋음 | 최소자승 (순위 부족 가능) |
JacobiSVD | 일반 | 느림 | 최상 | 의사 역행렬, SVD |
BDCSVD | 일반 | 중간 | 최상 | 대규모 SVD |
로봇공학에서의 선택 지침은 다음과 같다.
- 관성 행렬 \mathbf{M}(\mathbf{q})\ddot{\mathbf{q}} = \mathbf{b} 풀이:
LLT가 최적이다. \mathbf{M}이 SPD이므로 피벗팅이 불필요하며, Cholesky 분해의 계산량이 LU 분해의 절반이다. - 자코비안 기반 역기구학 \mathbf{J}\dot{\mathbf{q}} = \dot{\mathbf{x}}: 과잉 구동 로봇에서는
ColPivHouseholderQR또는JacobiSVD가 적절하다. - 접촉 역학의 KKT 시스템:
LDLT가 대칭 부정치 행렬을 처리할 수 있다.
희소 행렬 풀이
Eigen은 Eigen::SparseMatrix<double> 타입과 함께 희소 행렬 전용 분해를 제공한다.
| 분해 클래스 | 유형 | 행렬 조건 |
|---|---|---|
SimplicialLLT | 직접법 | 희소 SPD |
SimplicialLDLT | 직접법 | 희소 대칭 |
SparseLU | 직접법 | 희소 일반 |
SparseQR | 직접법 | 희소 일반 |
ConjugateGradient | 반복법 | 희소 SPD |
BiCGSTAB | 반복법 | 희소 일반 |
대규모 유한 요소 해석이나 접촉 문제에서 이 희소 풀이기가 활용된다. 반복법 풀이기에는 Eigen::DiagonalPreconditioner(대각 전처리)와 Eigen::IncompleteLUT(불완전 LU 전처리)를 조합할 수 있다.
기하학적 변환 모듈
Eigen의 Geometry 모듈은 로봇공학에서 필수적인 3차원 기하학적 변환을 지원한다.
회전 표현
Eigen은 다양한 회전 표현을 제공하며, 이들 간의 변환이 자유롭다.
| 클래스 | 표현 | 저장 크기 | 특성 |
|---|---|---|---|
Matrix3d | 회전 행렬 \mathbf{R} \in SO(3) | 9개 | 합성이 행렬 곱, 직관적 |
Quaterniond | 단위 쿼터니언 \mathbf{q} \in S^3 | 4개 | 보간 용이, 짐벌 락 없음 |
AngleAxisd | 축-각도 (k, \theta) | 4개 | 물리적 해석 직관적 |
쿼터니언 \mathbf{q} = w + xi + yj + zk에 의한 벡터 회전은 다음과 같이 수행된다.
\mathbf{v}' = \mathbf{q} \mathbf{v} \mathbf{q}^{-1}
Eigen에서 이 연산은 쿼터니언-벡터 곱 연산자로 제공되며, 내부적으로 회전 행렬을 거치지 않고 직접 계산하여 효율적이다. 연산량은 회전 행렬 곱의 15 FLOP에 비해 쿼터니언 회전은 약 24 FLOP이지만, 메모리 사용량과 보간 효율 면에서 쿼터니언이 유리한 경우가 많다.
2.2 강체 변환
강체 변환(rigid body transformation)은 회전과 병진의 조합으로, SE(3) 그룹의 원소이다. Eigen의 Isometry3d는 4 \times 4 동차 변환 행렬로 내부 표현된다.
\mathbf{T} = \begin{bmatrix} \mathbf{R} & \mathbf{p} \\ \mathbf{0}^\top & 1 \end{bmatrix} \in SE(3)
여기서 \mathbf{R} \in SO(3)은 회전 행렬, \mathbf{p} \in \mathbb{R}^3은 병진 벡터이다. 두 변환의 합성은 행렬 곱으로 수행된다.
\mathbf{T}_{02} = \mathbf{T}_{01}\mathbf{T}_{12}
Eigen은 Isometry3d의 곱셈을 일반 4 \times 4 행렬 곱이 아닌 SE(3) 구조를 이용하여 최적화하므로, 마지막 행의 [0, 0, 0, 1] 연산을 생략한다.
3. 주요 로봇공학 라이브러리에서의 Eigen 활용
3.1 Pinocchio
Pinocchio는 Eigen을 기반으로 한 로봇 동역학 라이브러리로, RNEA, CRBA, ABA 등의 알고리즘을 Eigen 타입으로 구현한다. Pinocchio의 핵심 데이터 구조는 다음과 같다.
- 관절 변수:
Eigen::VectorXd q, v, a - 관성 행렬:
Eigen::MatrixXd M - 자코비안:
Eigen::MatrixXd J(크기 6 \times n_v) - 프레임 변환:
pinocchio::SE3(내부적으로Eigen::Matrix3d와Eigen::Vector3d사용)
Pinocchio는 Eigen의 고정 크기 타입을 적극적으로 활용하여, 각 링크의 6 \times 6 공간 관성(spatial inertia), 4 \times 4 변환 행렬 등을 고정 크기로 처리한다.
3.2 KDL(Kinematics and Dynamics Library)
KDL은 Orocos 프로젝트의 일부로, 로봇의 기구학과 동역학 계산을 제공한다. KDL은 자체적인 벡터/행렬 타입을 사용하지만, Eigen과의 변환 인터페이스를 제공한다.
3.3 Drake
MIT에서 개발한 Drake는 로봇 시뮬레이션과 최적화 프레임워크로, Eigen을 핵심 선형대수 백엔드로 사용한다. Drake의 MultibodyPlant는 Eigen 타입을 통해 동역학 연산 결과를 반환하며, 자동 미분(auto-differentiation)을 위해 Eigen::AutoDiffScalar를 활용한다.
4. 성능 최적화 기법
4.1 메모리 정렬
Eigen의 고정 크기 타입(16바이트 이상)은 SIMD 명령어의 효율적 실행을 위해 16바이트 정렬(alignment)을 요구한다. Eigen::aligned_allocator를 사용하여 STL 컨테이너에서의 정렬을 보장하여야 하며, C++17 이상에서는 alignas 키워드를 활용할 수 있다.
정렬되지 않은 메모리에서 SIMD 연산을 수행하면 성능 저하(비정렬 로드/스토어) 또는 세그멘테이션 오류가 발생할 수 있으므로 주의가 필요하다.
4.2 .noalias()의 활용
행렬 곱 \mathbf{C} = \mathbf{A}\mathbf{B}에서 \mathbf{C}가 \mathbf{A}, \mathbf{B}와 메모리를 공유하지 않음을 보장할 수 있는 경우, .noalias()를 사용하면 Eigen이 임시 행렬 없이 직접 결과를 기록한다.
별칭(aliasing) 문제가 없는 경우 .noalias()를 사용하면 임시 행렬 할당과 복사를 제거하여, 소규모 행렬에서 유의미한 성능 향상을 얻을 수 있다.
4.3 맵(Map)을 통한 제로 카피 접근
외부 메모리 버퍼(센서 드라이버, 공유 메모리, GPU 버퍼 등)의 데이터를 Eigen 행렬로 감싸서 복사 없이 접근할 수 있다. Eigen::Map<MatrixType>은 기존 메모리 위에 Eigen 행렬 인터페이스를 제공한다.
이 기법은 실시간 시스템에서 메모리 복사 비용을 제거하여 지연 시간을 줄이는 데 유용하다. 특히 관성 측정 장치(IMU) 데이터나 관절 엔코더 데이터를 Eigen 벡터로 변환할 때 복사 오버헤드 없이 바로 연산에 활용할 수 있다.
4.4 컴파일 최적화 플래그
Eigen의 성능은 컴파일러 최적화 수준에 크게 의존한다. 권장되는 컴파일 플래그는 다음과 같다.
| 플래그 | 효과 |
|---|---|
-O2 또는 -O3 | 인라인화, 루프 최적화 |
-march=native | 호스트 CPU의 모든 SIMD 확장 활용 |
-DNDEBUG | 어설션 및 범위 검사 제거 |
-DEIGEN_NO_DEBUG | Eigen 내부 디버그 검사 제거 |
-funroll-loops | 추가적인 루프 전개 |
디버그 빌드(-O0)에서 Eigen의 성능은 릴리스 빌드에 비해 10~100배 느릴 수 있다. 이는 표현 템플릿의 다단계 함수 호출이 인라인화되지 않기 때문이다. 따라서 성능 프로파일링은 반드시 릴리스 빌드에서 수행하여야 한다.
5. 자동 미분과의 통합
현대 로봇공학에서 최적화 기반 제어와 경로 계획은 동역학 함수의 도함수(자코비안, 헤시안)를 요구한다. Eigen은 Eigen::AutoDiffScalar를 통해 자동 미분을 지원하며, Eigen 행렬 연산에 대해 전방 모드(forward mode) 자동 미분을 수행할 수 있다.
행렬 타입의 스칼라를 AutoDiffScalar로 변경하면, 동일한 알고리즘 코드로 함수값과 도함수를 동시에 계산할 수 있다. 이는 수치 미분(finite difference)보다 정확하고, 해석적 미분보다 구현이 간편하다는 장점이 있다.
CasADi, CppAD 등의 자동 미분 라이브러리도 Eigen과 호환되며, 로봇 동역학의 미분 계산에 널리 사용된다. Pinocchio는 cppad 및 casadi 스칼라 타입을 지원하여, 동역학 알고리즘의 해석적 도함수를 자동으로 생성할 수 있다.
참고문헌
- Guennebaud, G., & Jacob, B. (2010). “Eigen v3.” http://eigen.tuxfamily.org.
- Carpentier, J., et al. (2019). “The Pinocchio C++ library: A fast and flexible implementation of rigid body dynamics algorithms and their analytical derivatives.” IEEE International Symposium on System Integration (SII).
- Tedrake, R., & the Drake Development Team (2019). “Drake: Model-based design and verification for robotics.” https://drake.mit.edu.
- Golub, G. H., & Van Loan, C. F. (2013). Matrix Computations (4th ed.). Johns Hopkins University Press.
- Siciliano, B., Sciavicco, L., Villani, L., & Oriolo, G. (2009). Robotics: Modelling, Planning and Control. Springer.
v 0.1