Cortex 계열을 사용하다보면 느끼는 점은, 예전 ARM 920 시절보다
참 많이 빨라지고 좋아졌다 라는 느낌이다.
적어도 정렬문제 (Byte alignment)에서 해방되었다는 것만 해도 큰 축복 같았다.
(이 부분에 대해서는 다음에 한번 글을 써보기로 하겠다.)
현업에서 제품을 개발하는 분들은 공감하실수 있는 부분이지만,
사용자 분들의 요구치는 항상 하드웨어 발전 속도를 앞지르는 것 같다.
특히 내가 있는 영상 음성 보안 분야의 경우 SOC 선택의 폭이 좁아지면서
비슷한 하드웨어로 가격과 성능을 경쟁하는 상태가 되었다.
즉, 같은 하드웨어로 누가 더 많은 성능을 뽑아내어 보다 많은 소비자의 요구 기능을 뽑아내는지 경쟁이 시작된 것이다.
(솔직히.. 이것때문에 매우 힘들다. 일부 업체의 경우 안정성까지 희생해 가면서 성능을 뽑아내는데, 이런 제품을 본 소비자 분들은 저 회사 제품과 같은 성능에 안정성까지 요구하시게 된다. 물론 동일한 가격으로.. ㅜ_ㅜ)
가격이 고정되어 하드웨어를 손 볼 수 없다면 소프트웨어를 손봐서라도 고객의 요구를 만족시켜드려야 프로(?)라 할 수 있지 않겠는가... 라는 자조 섞인 말로 암시를 걸면서 개발자의 본분(?)인 삽질을 열심히 하고 있다.
성능을 조금이라도 더 뽑아낼 수 있는 부분이 있을까를 확인하기 위하여 시간을 핑계로 대강만 봐왔던 Cortex를 면밀하게 검토해 봤다.
헉.. 모든 Cortex 가 곱셈과 나눗셈이 CPU에 내장된 것이 아니었어?!
적어도 Cortex - M 시리즈는 몰라도 Cortex - A시리즈는 되있을 거라 생각했는데 그건 아니었다.
[Cortex 코어별 나눗셈 지원 여부]
Cortex | Thumb 모드 지원 | 일반모드지원 |
A57 | 지원 | 지원 |
A53 | 지원 | 지원 |
A15 | 지원 | 지원 |
A9 | 지원안함 | 지원안함 |
A8 | 지원안함 | 지원안함 |
A7 | 지원 | 지원 |
A5 | 지원안함 | 지원안함 |
R7 | 지원 | 지원 |
R5 | 지원 | 일부 지원 |
R4 | 지원 | 지원안함 |
M4 | 지원 | 지원안함 |
M3 | 지원 | 지원안함 |
M1 | 지원안함 | 지원안함 |
M0 | 지원안함 | 지원안함 |
M0+ | 지원안함 | 지원안함 |
헉.. Cortex A9 와 Cortex A8이 나눗셈 명령 (SDIV/UDIV)가 기본지원이 아니라고!?
불행히도 내가 쓰는 SOC의 상당수 코어가 Cortex A9와 Cortex A8이었다.
Cortex가 기본적으로 나눗셈 instruction 을 지원하여 왠만해서는 25클럭을 넘지 않으리라 생각하고 만든 부분들을 전면적으로 재 검토 하여야 했다.
C++에서 나눗셈의 제수가 상수인 부분이야 쉬프트 계열 명령으로 자동 대체 되겠지만,
아닌 부분들은 뺄셈으로 구현하는 수 밖에 없게 된다.
그렇다고 피제수가 제수보다 작아질때까지 무조건 루프돌며 빼는 끝을 기약하기 힘든 형태로 뺀다면 32비트 기준으로 최대 40억회 가량의 루프를 돌아야 한다.
클럭으로 따지면 400억 클럭정도까지 소요될 수도 있다는 뜻이다.
그래서 일반적으로 제수와 피제수의 크기차이를 확인하고, 이에따라 방법을 결정하는데 비트 위치에 맞추어 빼는 형식으로 루프 회수를 줄인다.
예를 들어 14112 를 11로 나누는 경우, 무조건 빼가며 루프를 돌 경우 1282번의 루프를 돌아야 한다.
하지만 비트 위치에 맞추어 빼는 형식으로 하면 아래와 같다.
계산은 2진수 기준으로 한다.
14112 는 2진수로 11011100100000 (2진수 14자리)
11은 2진수로 1011 (2진수 4자리)
(루프 1회 - 13번 비트부터 제수와 동일한 4자리 비교)
11011100100000 -> 101100100000 (피제수)
10110000000000 (제수)
10000000000 (몫) 피제수가 더 크므로 1
(루프 2회 - 12번 비트)
0101100100000 (피제수)
1011000000000 (제수)
10000000000 (몫) 제수가 더 크므로 0
0101100100000 -> 100000 (피제수)
101100000000 (제수)
10100000000 (몫) 피제수가 더 크므로 1
00000100000 (피제수)
10110000000 (제수)
10100000000 (몫) 제수가 더 크므로 0
(루프 5회 - 9번 비트)
0000100000 (피제수)
1011000000 (제수)
10100000000 (몫) 제수가 더 크므로 0
0000100000 (피제수)
1011000000 (제수)
10100000000 (몫) 제수가 더 크므로 0
(루프 6회 - 8번 비트)
000100000 (피제수)
101100000 (제수)
10100000000 (몫) 제수가 더 크므로 0
101100000 (제수)
10100000000 (몫) 제수가 더 크므로 0
(루프 7회 - 7번 비트)
00100000 (피제수)
10110000 (제수)
10100000000 (몫) 제수가 더 크므로 0
10110000 (제수)
10100000000 (몫) 제수가 더 크므로 0
(루프 8회 - 6번 비트)
0100000 (피제수)
1011000 (제수)
10100000000 (몫) 제수가 더 크므로 0
1011000 (제수)
10100000000 (몫) 제수가 더 크므로 0
(루프 9회 - 5번 비트)
100000 (피제수)
101100 (제수)
10100000000 (몫) 제수가 더 크므로 0
101100 (제수)
10100000000 (몫) 제수가 더 크므로 0
(루프 10회 - 4번 비트)
100000 -> 1010 (피제수)
10110 (제수)
10100000010 (몫) 피제수가 더 크므로 1
10110 (제수)
10100000010 (몫) 피제수가 더 크므로 1
(루프 11회 - 3번 비트)
1010 (피제수)
1011 (제수)
10100000010 (몫) 제수가 더 크므로 0
1011 (제수)
10100000010 (몫) 제수가 더 크므로 0
위와 같이 11회의 루프에 연산을 마쳤다.
물론 사전에 bit 개수를 알아내는 작업이 필요하거나, 무조건 32 - 제수 비트수 만큼의 루프를 돌아야 하지만, 최대 32회의 루프면 충분하다.
그래도 CPU에서 직접 지원하는 명령보다 매우 느리다는 점은 여전하다.
Cortex도 A15급이상이 아닌 이상 되도록이면 상수가 아닌 변수간 나눗셈 연산은 피하거나 쉬프트로 대치하는것이 좋다는 것을 알게 되었다.
왠만하면 코드 재 사용을 하기 위하여 어셈블리는 되도록 손을 대지 않으려 하지만, 상황은 나에게 계속 어셈블리를 할 수 밖에 없는 상황이 되어가는것 같다.
1999년 경 C / C++을 잘 몰라 순수 어셈블리로 32bit OS를 개발했던 적이 있었다.
GPU 도움을 받지 못해 UI표현 속도가 너무 느려, 어디까지나 MMU 제어와 GUI, 멀티미디어에 대한 스터디 차원의 일이었지만, 이 작업이 인정받아 서울의 한 OS개발 프로젝트를 진행하는 개발사에 취직한 적이 있었다.
이 때 월요일에 출근해서 토요일까지 감금당하다 시피 일을 하다보니, 젊은 나이에 쇼크가 컷던지 OS 개발 분야를 포기하고, 지금과 같이 영상 , 음성 보안이나 신호처리로 전향하게 된 것 같다.
이 분야는 여러가지 SOC를 사용해야 하다보니 효율성을 위하여 재 사용 가능한 코드 개발을 지향하게 되었고, 때문에 어셈블리 사용을 최대한 자재하게 되었다.
세상은 그런 게으름(?)을 용납하지 않으려는지 아키텍쳐간 최적화를 하라고 요구한다.
사용자의 요구를 훨 씬 뛰어넘는 CPU가 나오기 전까진 앞으로도 이 삽질을 계속 해야만 할것 같다...
PS: 블로그에서 표그리기 왜이리 힘들까요 ㅠ_ㅠ
댓글 없음:
댓글 쓰기