객체 지향 프로그래밍과 하드웨어 설계
HLS를 활용하여 C++ 언어로 하드웨어를 설계하면 객체 지향의 개념을 활용해 하드웨어를 모델링 할 수 있습니다. 객체 지향 프로그래밍은 소프트웨어에서는 보편적인 프로그래밍 방식인데요, 그렇다면 하드웨어 역시 객체 지향으로 설계하는 것이 좋을까요?
객체 지향 프로그래밍이 항상 더 좋을까?
먼저 하드웨어를 이야기하기 전에, 소프트웨어 프로그래밍에서는 항상 객체 지향 프로그래밍이 좋을까요?
아닙니다. 애플리케이션 종류에 따라 다른 프로그래밍 스타일이 더 적합할 수도 있습니다. 프로그래밍 스타일은 아래와 같이 다섯 개로 분류할 수 있는데요, 애플리케이션에 따라 각각 다른 프로그래밍 방식이 더 좋을 수 있습니다.
- Procedure-oriented
- Object-oritented
- Logic-oriented
- Rule-oriented
- Constraint-oriented
예를 들어, Computation intensive 한 애플리케이션은 object-oriented 프로그래밍 보다 procedure-oriented 프로그래밍 방식이 더 적합할 수 있습니다.1 이러한 차이는 procedure-oriented 프로그래밍 방식과 object-oriented 프로그래밍 방식이 플리케이션을 바라보는 관점의 차이에서 비롯됩니다. 문장을 구성하는 가장 중요한 두 가지는 명사와 동사입니다. 프로그래밍 언어 역시 똑같습니다. 변수와 구조체를 명사라고 한다면 연산자와 함수는 동사라고 할 수 있죠. procedure-oriented 프로그램은 동사를 중심으로, object-oriented 프로그램은 명사를 중심으로 구성됩니다. 같은 걸 표현하더라도 아래와 같이 다르게 설명하죠.
// procedure-oriented
move(car);
// object-oriented
car.move();
procedure-oriented 프로그래밍에서는 move()
라는 함수가 자동차를 움직입니다. 반면 객체 지향 프로그램에서는 car
라는 자동차가 움직이죠. 따라서 명사(데이터)가 중요한 프로그램은 object-oriented 방식으로, 동사(계산)이 중요한 프로그램은 procedure-oriented 방식으로 프로그래밍 하는 것이 좋습니다.
예를 들어, FFmpeg이나 SVT-AV1같이 비디오 쪽에서 널리 쓰이는 프로젝트들은 요즘에도 procedure-oriented 프로그래밍 방식을 사용하고 있죠. 반면에 게임은 어떨까요? 게임을 만들 때 객체 지향 프로그래밍을 하지 않는다면 엄청난 고생이 뒤따를 것은 눈에 보입니다. 수많은 캐릭터나 유닛들의 데이터와 행동들을 전부 따로 분리해서 표현해야 하기 때문이죠. 요약하자면, 만들려고 하는 프로그램에 따라 다른 접근 방식을 사용해야 한다는 것입니다.
하드웨어는 어떤 설계 방식이 좋을까?
하드웨어 역시 경우에 따라 객체를 사용하는 것이 효과적인 경우도 있고 그렇지 않은 경우도 있습니다. 데이터가 중요한 하드웨어, 예를 들어 라인 버퍼나 FIFO, 복잡한 레지스터 같은 것들은 객체로 만드는 것이 좋습니다. 이것들 말고 계산을 하는 모듈들, 예를 들어 FIR 모듈이나 FFT 모듈 같은 것들은 함수로 구현하는 것이 좋습니다.
여기서 드는 의문은, 보통 라인 버퍼나 레지스터 같은 것들은 FIR 모듈이나 FFT 모듈 같은 모듈들 내부에 있는 경우가 많죠. 위에서 말씀드린 방식을 따르자면 함수 내에서 객체를 선언하고 사용해야 하므로 procedure-oriented 방식과 object-oriented 방식을 혼용해서 써야 합니다. 이렇게 서로 다른 방식을 섞어서 사용해도 될까요?
결론을 말하자면 당연히 됩니다.
Procedure-oriented 방식과 object-oriented 방식은 경쟁적인 관계가 아닙니다. 오히려 상호 보완적인 관계이며 서로 잘 합쳐질 수 있습니다.2 이미 객체 지향 방식을 사용하지 않더라도 많은 분들이 FIFO 라이브러리를 사용하고 있는데, FIFO는 클래스로 정의되어 있고 procedure-oriented 방식과 함께 잘 사용하고 있습니다. 또한 HLS 툴에서 제공하는 라이브러리들을 보더라도 메모리 관련된 것들은 클래스로 구현되어 있고 계산과 관련된 것들은 함수로 구현되어 있는 경우가 많죠.
Example
이제 예제를 살펴볼까요? ASIC의 경우 sigle-port SRAM을 사용하되, 매 사이클 read/write 가능한 라인 버퍼가 필요한 경우가 있습니다. 모든 것을 함수로 구현한 경우와 라인 버퍼를 객체로 구현한 경우를 비교해 볼게요.
Dual-port SRAM을 사용하면 매 사이클마다 write port에 데이터를 저장하고 read port에서 데이터를 읽을 수도 있지만, dual-port SRAM이 single-port에 비해 area가 많이 크기 때문에 ASIC에서는 가능한 경우 single-port로 구현합니다. Dual-port 대신 sigle-port SRAM의 bitwidth를 두 배 크게 잡고 짝수 번째 인덱스에서는 두 개의 데이터 READ, 홀수 번째 인덱스에서는 두 개의 데이터를 WRITE 하면 매 사이클 read/write 하는 것처럼 구현이 가능합니다.
다만 FPGA의 경우에는 이미 dual-port SRAM(BRAM)이 많이 있기 때문에, 그냥 BRAM을 simple dual-port로 사용합니다.
함수만 사용한 경우
void processing(ac_channel<ac_int<8>> &chan_input, ac_channel<ac_int<8>> &chan_output) {
ac_int<16> mem[1920];
ac_int<8> read_cache;
ac_int<8> write_cache;
for (int r = 0; r < 1080; r++) {
for (int c = 0; c < 1920; c++) {
ac_int<8> input = chan_input.read();
ac_int<8> linebuf_data;
if (index % 2) {
// read from cache, write to sram
} else {
// read from sram, write to cache
}
ac_int<8> output;
// Perform data processing using input and linebuf_data
// ...
chan_output.write(output);
}
}
}
라인 버퍼를 객체로 구현한 경우
class LineBuffer {
public:
ac_int<8> run(int index, ac_int<8> input) {
if (index % 2) {
// read from cache, write to sram
} else {
// read from sram, write to cache
}
}
private:
ac_int<16> mem[1920];
ac_int<8> read_cache;
ac_int<8> write_cache;
};
void processing(ac_channel<ac_int<8>> &chan_input, ac_channel<ac_int<8>> &chan_output) {
LineBuffer lineBuffer;
for (int r = 0; r < 1080; r++) {
for (int c = 0; c < 1920; c++) {
ac_int<8> input = chan_input.read();
ac_int<8> linebuf_data = lineBuffer.run(c, input);
ac_int<8> output;
// Perform data processing using input and linebuf_data
// ...
chan_output.write(output);
}
}
}
위 코드에서는 많은 부분이 생략되어 있어 큰 차이가 느껴지지 않지만 프로젝트의 규모가 커질수록 객체를 사용한 게 함수만 사용한 것보다 여러 면에서 낫습니다.
-
가독성 증가
읽기 쉬운 코드는 한 번에 한 부분을 제대로 이해할 수 있도록 나누어져 있습니다. 객체로 구현한 경우 프로세싱 부분과 메모리 부분이 명확하게 나누어져 있으므로, 필요한 부분을 나눠서 이해할 수 있습니다. 관련된 것들이 한곳에 모여 있어 이해하기가 쉽죠. 반면에 함수로 구현한 경우는, 코드를 처음 보는 사람은 전체 구현을 한 번에 이해해야 합니다.
-
유지 보수 용이성
함수로 구현한 경우는 라인 버퍼 내부에서 사용하는 변수들(
read_cache
,write_cache
,mem
)이 프로세싱 하는 부분에 노출되어 있습니다. 최악의 경우는 라인 버퍼가 사용하는 변수들을 프로세싱 하는 부분에서 사용하는 것입니다. 코드가 이렇게 모든 부분이 서로 서로 연결되어 있으면 유지 보수하기가 매우 어려워집니다. 예를 들어 자동차의 엔진이 고장 났을 때, 엔진만 수리하거나 교체하면 되죠. 그런데 모든 부분이 서로 연결되어 있는 경우는, 엔진의 기능이 변경되면 관련된 브레이크 시스템, 조향 장치, 배기 시스템에 모두 영향을 주기 때문에 모든 시스템을 전부 이해한 뒤 한 번에 고쳐야 합니다. -
재사용성 증가
라인 버퍼를 구현했는데, 다른 모듈에서도 사용하고 싶으면 어떻게 해야 될까요? 함수를 사용한 경우에는 라인 버퍼에 필요한 변수들만을 골라서 복사해서 넣고, 필요한 코드들도 파악해서 복사 후 붙여 넣어야 합니다. 그러나 클래스를 사용한 경우는 그냥 클래스 코드만 가져오면 됩니다.
References
Leave a comment