Conclusion
이제 앞에서 설명한 문제의 원인에대해 설명해보겠습니다.
이미 예측한분도 계실테지만 결론부터 이야기하면 memory alignment 문제 입니다. 끝!
여기서 끝내면 너무 허무하니, 좀더 자세히 살펴보겠습니다.
memory alignment?
- 많이들 알고 계실겁니다. struct, 즉 구조체를 정의하면 padding이 붙는경우가 있다는것을... 그때의 padding이 alignment와 관계됩니다. 각 구조체의 멤버는 각타입별 변수크기의 배수로 메모리주소가 정렬되게 됨으로 앞에 padding이 붙게 됩니다. 이것을 natural address로 부릅니다. 또한 구조체의 마지막 멤버는 특정 크기의 padding이 뒤에 붙게되고요(GCC를 보니 구조제의 마지막 멤버의 PADDING은 구조체멤버중 사이즈가 가장큰 맴버의 사이즈로 정렬이 되게 되어있네요.)
- 즉, natural address alignement라는것은, 1 byte의 char형은 어떤메모리에도 aligned memory가 되며, 2 byte의 short형은 메모리의 시작주소가 2의 배수이어야하고, 4 byte의 int형은 메모리의 시작주소가 4의 배수이어야하고, 8 byte의 long long형은 메모리의 시작주소가 8의 배수이어야 한다는것입니다.
- 근데 왜 이렇게 memory alignment를 맞춰줘야 할는걸까요? 그 이유는 CPU가 메모리를 관리하는 하드웨어적인 요소때문이라고 합니다. 프로그래머가 보는 메모리와 달리 CPU는 메모리를 2-bytes/4-bytes/8-bytes/16-bytes 심지어 32-bytes중 특정단위의 chunk단위로 봅니다(관리합니다). 이런 특징을 memory access granularity 라고 부릅니다. 그래서 CPU는 어떤 데이타를 LOAD 하거나 STORE 할때 절대 이 chunk가 겹치는 메모리주소에 한번에 LOAD하거나 STORE할수 없는것입니다. 예를들면 4-byte memory access granularity 의 아키텍쳐일때 메모리 0x3 번 주소에서 4바이트의 값을 한번에(load word) 읽어올려고 시도하면, CPU는 이를 정상적으로 처리하지 못하게 됩니다(아키텍쳐에 따라 이 이후의 동작은 다릅니다). 앞에서 설명했다시피 0x3번지의 4바이트 값은 0번 chunk 와 1번 chunk가 겹치게 되기 때문입니다.
- 그럼 왜 CPU는 메모리를 이렇게 관리할까요? 이것은 CPU가 제한된 소자를 사용하여 메모리에 접근하기위한 최적의 성능을 내기위함이라고 합니다. 그럼 컴파일러가 알아서 정렬된 주소로해서 2개의 chunk를 읽어와서 잘잘~ 조합해서 넘겨주면 되지 않나 생각할수도 있겠지만, 정적주소라면 가능하겠지만 동적으로 결정되는 가상주소라면 이또한 컴파일러입장에선 불가능합니다. 모든 READ/STORE 명령에 조건검사를 할수도 없는것이고... 또한 할수 있다하여도 다른 여러 이유들때문에 제약이 따르게 됩니다.
- 제약중 한가지를 예를들면 instruction의 atomic을 예를들수있습니다. 최신 모든 CPU는 atomic instruction을 제공합니다. 그래서 왜 atomic이 무슨 연관이 있는지 예를들어 보면, unalign된 메모리에 접근을 하여 2개 이상의 chunk를 가져와 조합을 해야할경우, 첫번째 chunk의 가상주소는 MMU에 의해 가상주소로 잘 변환되어 가져왔지만, 두번째 chunk의 가상주소는 MMU에 의해 정상적으로 가상주소로 변환이 안될수도있습니다. TLB/PTE에 변환 테이블이 없는경우인데, 이 경우는 page fault로 인해 제어가 리눅스 커널의 특정 코드로 넘어가게됩니다. 보통 이런 page fault가 나는 경우는 PAGE OUT된 경우입니다. 즉 SWAP된 경우입니다. 제어가 리눅스 커널의 다른 코드로 넘어가되면(아마 커널에서는 PAGE IN을 할겁니다) 이미 atomic이 깨지게 된것입니다. 정렬된 주소는 항상 같은 PAGE에 존재하기 때문에 atomic을 보장합니다. 그럼 32bit 아키텍쳐에서 8byte의 변수를 읽어오는것도 2번의 LOAD가 필요한데 이는 정렬된 주소는 항상 같은 PAGE에 있기 때문에 atomic을 보장하지 않을까? 라고 추측만 해봅니다..ㅎ
memory alignment는 알겠는데, 왜 CN6330,PC는 빠르게 잘 동작하고 MT7621은 느리게 동작할까요?
- 위에서 memory alignment가 맞지 않으면 CPU는 이를 바로 처리하지 못한다고 설명했었습니다. 아무튼 바로 처리를 하지 못해도 프로그램이 잘~ 도는걸 보면 처리는 하나봅니다.
- 일반적으로 이렇게 정렬되지 못한 주소의 접근은 unaligned memory access exception IRQ가 발생하여 커널코드로 그 제어가 넘어가게 됩니다. 그럼 해당 커널 코드는 atomic을 보장하기위해 LOCK을 잡고 정렬된 2개이상의 chunk를 읽어와 잘 조합하여 원하는 값을 만들어내게 되는것입니다. 그리고 원래 코드로 복귀하게 됩니다. 이런 동작은 수십 instruction으로 이루어져있으며 interrupt로 인한 이전의 register 저장/복원까지해서 많은 오버헤드가 일어나게 됩니다. MIPS wikipedia에 보니 최대 1400배까지 성능이 떨어질수있다합니다. 만약 1000바이트의 코드를 2byte씩 읽어와 원하는 곳에 쓰기 동작을 할때, 최악의 경우 읽기/쓰기 주소 모두 정렬되지 않은 주소라면 unalign memory access exception 이 500번 + 500번 해서 1000번이 일어나게 됩니다. 이렇게 해서 떨어진 성능표가 Test_1, Test_2 MT7621의 그 결과입니다. Test_1 , Test_2 에서 STORE주소는 C코드로 해서 정렬했지만 LOAD주소는 정렬되지 못했기 때문입니다. Test_3에서 잘나왔던것은 모든 주소가 alignment되어 있었기 때문입니다. MIPS의 예를들면 이런 unalign memory acces exception 을 처리하는 커널기능을 FIXADE라고 부릅니다. 그 함수는 do_ade() 입니다. 아키텍쳐 의존적인 이 함수는 ~/arch/mips/kernel/unaligned.c 에 존재하고 해당 unaligned meory access exception 이 발생했을때 do_ade()를 호출하라는것은 ~/arch/XXX/kernel/genex.S 에 macro로 정의 되어있습니다. 기본적으로 unaligned memory access exception이 일어났을때 커널은 이를 적적하게 처리하지만 MIPS에서 sysmips(MIPS_FIXADE, 0) 시스템콜을 이용하여 사용하지 않음으로 변경하면 어플리케이션은 SIGBUS 시그널을 받고 비정상종료되게 됩니다.
- X86 PC 또는 CN6330 같은 아키텍쳐가 MT7621과 다르게 빠르게 동작할수 있었던것은 unaligned memory access exception을 처리해주는 transistor가 따로 하드웨어적으로 존재하기 때문입니다. CN6330의 예를 들어보면 커널 설정중 CONFIG_CAVIUM_OCTEON_HW_FIX_UNALIGNED 라는 설정이 존재합니다. 해당 설정을 enable시켜주면 따로 하드웨어에서 처리해주게 됩니다. 심지어 3-cycle뿐이 소비하지도 않는다고 합니다.
- 결론
- 다시 처음으로 돌아가서 MT7621같이 unaligned memory access exception을 따로 처리해주는 하드웨어가 존재하지 않는 아키텍쳐에서는, 어떤 메모리 복사를 사용해야 좋을까요??
- 최초 문제점으로 돌아가 ESP패킷의 시작주소를 메모리 재할당 없이 정렬해주기 위해선 어떤방법을 사용해야할까요? 즉, 읽는 주소와 쓰는 주소가 모두 natural address로 정렬시켜야합니다. ESP패킷의 하위부분엔 outer IP header가 존재합니다(IP|ESP|PAYLOAD). 하여 outer IP header fmf 변조시키면 안되기 때문에 목적주소가 ESP패킷의 시작주소보다 커야합니다. 즉 Test_1 과 같은 상황이어야합니다. 목적주소가 더 크기 때문에 메모리 복사는 뒤에서부터 시작해야합니다. 복사해야하는 패킷의 크기가 고정되지 못하는 상황에서는 어떤식으로든 READ/STORE 주소가 정렬되지 않을 가능성이 크게 됩니다. 하여 2-byte 이상의 메모리복사는 성능을 많이 떨어트리게 됩니다. 그럼 1-byte 씩 복사하면? 물론 unaligned memory access exception을 발생하지 않지만 1400번의 loop로 인한 오버헤드가 생기게 됩니다. 그래서 정 메모리 정렬을 신경쓰기 싫다면 메모리를 새로 할당하여 그곳에 복사하는것이 가장 좋은 성능을 가져다 줍니다.
- 기본적으로 memory 할당은 아키텍쳐의 cache line에 맞게 정렬하여 가상주소를 리턴해줍니다. MT7621의 경우 1 << 6 크기의 cache line을 가지고 있어, 항상 새로 할당받은 메모리는 64bytes로 정렬되게 됩니다(0x40 의 배수). 그러나 해당 메모리에 패킷을 복사할땐 조심해야할것이 하나 있습니다. ethernet/ip 환경일경우 ethernet header의 크기 14bytes + ip header의 크기 20bytes(옵션이 없다는 가정하에) 를 가지는데 ethernet header의 크기가 14byte라는 애매한(1과 2의배수만됨) 크기를 가지게됨으로 4바이트 정렬은 되지 못하게 됩니다. 그래서 ~/include/skbuff.h 에 #define NET_IP_ALIGN 2 라고 정의된 2를 패킷의 메모리 할당시 추가하여 메모리 할당을 하게됩니다. 그래서 보통 IP헤더 앞에 2바이트의 패딩을 두고(또는 간혹 ethernet헤더앞에) 정렬을 맞추게 됩니다.
- 결론은 메모리 복사의경우, 복사할 데이타의 양이 가변적이라면 차라리 새로운 메모리를 할당하여 사용하는것이 더 좋은 성능을 가져다 준다 입니다. 하지만 패킷의 특성상 항상 ESP의 시작주소는 2의 배수로 시작하고(raeth 스위치: 8정렬 + 14bytes + 4bytes + 20byte , r8168 : 2정렬 +14bytes + 20bytes) ESP패킷의 특성을 보면 암호화된 payload의 크기는 암호화 block의 배수가되며, 인증 trailer도 12 또는 16의 크기로 2의 배수를 항상가지게 됩니다. 하여 ESP의 시작주소도 2의 배수이고 복사할 데이타의 크기도 항상2 의 배수입니다. 또한 4의 배수로 정렬할려할때 유일하게 정렬되지 않은 주소는
2의 배수이지만 4의 배수가 아닌 주소
로 shift 또한 2바이트만 하게 됩니다. 하여 MT7621의 ESP패킷의 4-aligne정렬은 short형태로 2-bytes씩 복사하여도 절대 unaligned memory access exception이 발생하지 않기에 MT7621의 ESP 패킷의 4-align 메모리주소 정렬은 2byte씩 복사하기로 결정되었습니다.