Objective-C와 Swift에서 NSData(Data) 데이터 단순 탐색

Objective-C 코드 (bytes는 NSData 클래스 객체에서 bytes를 호출하여 얻은 데이터 포인터)

for(uint32_t i = 0; i < dataSize - 4; i++)
{
    uint32_t startCode = *(uint32_t *)(bytes + i);

    if(startCode == 0x01000000)
    {
        [points addObject:[NSValue valueWithPointer:bytes + i]];
        [nalSizes addObject:[NSNumber numberWithInt:4]];
        
        i += 3;
    }
    
    if((startCode & 0x00FFFFFF) == 0x00010000)
    {
        [points addObject:[NSValue valueWithPointer:bytes + i]];
        [nalSizes addObject:[NSNumber numberWithInt:3]];
        
        i += 2;
    }
}

 

Swift 코드 (3.0으로 빌드, streamData는 Data 클래스의 객체)

streamData.withUnsafeBytes { (p8: UnsafePointer) in
    while i < count
    {
        if p8.advanced(by: i + 0).pointee == 0 && p8.advanced(by: i + 1).pointee == 0
        {
            if p8.advanced(by: i + 2).pointee == 0 && p8.advanced(by: i + 3).pointee == 1
            {
                offsets.append(i)
                nalSizes.append(4)
                
                i += 4
                
                continue
            }
            if p8.advanced(by: i + 2).pointee == 1
            {
                offsets.append(i)
                nalSizes.append(3)
                
                i += 3
                
                continue
            }
        }
        i += 1
    }
}

 

H264에서 NAL 헤더를 찾는 코드이다. 어쩌다보니 기존에 Objective-C로 코드를 만들어놓은걸 Swift로 옮기다보니... 생각보다 삽질을 많이하게되었다. 프레임워크는 동일하지만 Swift는 기본적으로 포인터 연산을 지원하지않기때문에 C에서의 구조체, 바이너리 데이터를 int형식으로 형변환하여 비교하는 등의 연산에서 자유롭지않아보인다. 동일한 LLVM에서 컴파일되고 1:1 호환 가능하다고는하지만 위와같은 사항 외에도 언어의 특성이있어서 목적은 동일하지만 결국은 코드의 구조는 달라졌다.

딱봐도 Swift의 코드가 비효율적으로 보이지만 초당 1메가정도의 데이터에서 CPU점유율을 보면 1~2%정도만 더 Swift에서 많이 사용한다. 아직 릴리즈 빌드는 찾아본적없어서 export하게되면 또 차이날지는 모르겠지만... 현재로서는 약 2~3시간 삽질을 한 결과 위 코드가 Objective-C의 코드와 가장 근접한 CPU 사용률을 보이는 코드이다.

아래는 순서대로 Objective-C, Swift 코드에서의 CPU 사용률

VideoToolbox 디코더에서 픽셀 포맷을 BGRA와 420YpCbCr를 사용할때의 CPU 점유율 차이

일단 각각의 차이

kCVPixelFormatType_32BGRA 값을 사용할 때

kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 값을 사용할 때

하드웨어 스펙

420YpCbCr 색상으로 출력할 때 평균적으로 3~4%정도 점유율이 더 적다. Metal 텍스쳐 두개를 생성하고 GPU에서 색상변환하기위해 소비되는 연산보다 VideoToolbox에서 BGRA로 변환하여 CVImageBufferRef 객체를 생성하는 부하가 더 큰것으로 보인다.

맥에서 어느정도 끝나면 iOS앱에도 Metal을 사용해서 얼마나 차이나는지 비교해봐야겠다.

Video Decode Acceleration Framework 사용 시 주의점

최근 거의 2주일동안 apple의 하드웨어 H264 디코더인 VDA와 씨름을 하였다. 이건 H264의 깊이있는 지식을 가지고 쓴것이 아니고 단지 VDA로 하드웨어 디코딩을 하기위해 중점을 맞춘것이니 깊이있는 지식은 넘어가고 VDA나 VideoToolbox를 사용하기위해 참고하는 정도만으로 넘어가야 할 것이다.

VDA에 관한 기술문서로 https://developer.apple.com/library/content/technotes/tn2267/_index.html 페이지가 거의 유일하며, 그나마 참조할건 http://stackoverflow.com/questions/29525000/how-to-use-videotoolbox-to-decompress-h-264-video-stream 이 글의 댓글이 거의 유일하다. (물론 VideoToolbox와 VDA는 초기화 방법이 서로 다르지만, VDA에서 지원되는 기능은 VideoToolbox에서도 동일하게 지원하는거로 보이고 avcC라는 것을 생성하지 않아도 되기때문에 초기화가 더 수월하다.)

먼저 이론적인 기본은 위 stackoverflow의 댓글로 확인하면 될것이다. 약간 첨언한다면 H264는 3바이트, 또는 4바이트짜리 Start Code라고 불리는 코드로 각각의 프레임을 구분한다. 그리고 디코딩에 필요한 NALU 타입은 7 (SPS), 8 (PPS), 5 (IDR), 1 (non-IDR) 정도로 보인다.

디코더를 초기화하기 위해서는 SPS, PPS가 필요하며, 이 정보를 바탕으로 디코딩을 한다. (영어가 된다면 https://developer.apple.com/videos/play/wwdc2014/703/ 이것이 참고가 될것이고, 프레젠테이션 자료는 Resource 탭에서 받을 수 있다.) Continue reading Video Decode Acceleration Framework 사용 시 주의점

닷넷 Any CPU 빌드한 바이너리가 64비트 OS에서 32비트로 동작할 때

닷넷으로 Text to Speech. 즉 입력 된 텍스트를 읽어주는 TTS를 해달라는 부탁으로 시작하게 된 삽질.

내가 사용하는 OS는 윈도 10이고 타겟은 윈도 7이였다. 윈도 TTS는 https://msdn.microsoft.com/en-us/library/hh361572(v=office.14).aspx 이곳에서 Runtime, Language Packs 두가지를 설치하면 된다고 되어있는데… 이상하게 아무리 재설치를해도 한글 음성이 지원되는 Voice 객체가 나타나지않았다.

윈도 제어판에서는 한글 음성이 나타나는데 닷넷에선 아무리 해도 나타나지않아 포기하고 C++로 Voice 객체를 조회해보았지만 역시나 실패. 혹시나해서 64비트로 빌드하여 다시 조회하니 Heami가 나타나는 것이였다.

그런것이였다. 윈 10은 32비트, 64비트 둘 다 한글 Voice가 재공되는 것이였다… 근데?! 뭐지? 싶어 닷넷 실행파일 띄워놓고 작업관리자 열어보니 32비트로 실행되고있고, 구글링해보니 http://stackoverflow.com/a/23351613 이 댓글이 나왔고, 프로젝트 속성을 보니 아래 옵션이 있었다.

2016-10-16-1

32비트 기본 사용…!!! 32비트 기본이라니! 하.하!? VS2015에서 기본으로 체크되는 옵션인지 어떤지는 모르겠지만 회사에서 VS2015가 나오자마자 구입하였는데… 왠지 최근 만든 닷넷 어플들 32비트 모드로 동작하고있을거같다.

아마도 ActiveX라던지 COM Object, 기타 외부 네이티브 라이브러리가 32비트가 대다수라 생기는 문제가 많기때문에 이런걸 기본옵션으로 체크되어있는거같은데… 나같은 경우엔 그 반대라서 하루 삽질을 하였다.

아… 해결되어서 좋긴한데 왜 눈물이 나려하지… 내 시간은..

Continue reading 닷넷 Any CPU 빌드한 바이너리가 64비트 OS에서 32비트로 동작할 때

shared_ptr 객체를 멀티스레드에서 사용할 때

분명 shared_ptr은 레퍼런스 카운트가 스레드 안정적이라고 되어있다고 나와있지만 아래와같은 코드를 실행하면 실행하자마자 에러가 발생한다.

#include <memory>

std::shared_ptr<int> g;

DWORD WINAPI U(void *)
{
    while (true)
    {
        std::shared_ptr<int> data = g;
    }
    return 0;
}

DWORD WINAPI C(void *)
{
    while (true)
    {
        std::shared_ptr<int> data = std::make_shared<int>(0);

        g = data;
    }
    return 0;
}

int main()
{
    HANDLE h1 = CreateThread(NULL, 0, U, NULL, 0, NULL);
    HANDLE h2 = CreateThread(NULL, 0, C, NULL, 0, NULL);

    WaitForSingleObject(h1, -1);
    WaitForSingleObject(h2, -1);

    return 0;
}

거의 수시간동안 구글링해도 한결같이 나오는건 레퍼런스 카운트는 스레드 안전하다는 것 정도.
그래도 다행이도 계속 검색해보니 결국 아래와같이 atomic을 사용하는 코드가 나왔다.

#include <atomic>
#include <memory>

std::shared_ptr<int> g;

DWORD WINAPI U(void *)
{
    while (true)
    {
        std::shared_ptr<int> data = std::atomic_load(&g);
    }
    return 0;
}

DWORD WINAPI C(void *)
{
    while (true)
    {
        std::shared_ptr<int> data = std::make_shared<int>(0);
        std::atomic_store(&g, data);
    }
    return 0;
}

int main()
{
    HANDLE h1 = CreateThread(NULL, 0, U, NULL, 0, NULL);
    HANDLE h2 = CreateThread(NULL, 0, C, NULL, 0, NULL);

    WaitForSingleObject(h1, -1);
    WaitForSingleObject(h2, -1);

    return 0;
}

저렇게 atomic_store, atomic_load를 사용하니 이제 정상적으로 shared_ptr 객체가 두 스레드간의 참조가 문제없이 되었다. 분명 부스트의 shared_ptr 기준으로 코드를 보면 InterlockedIncrement 함수를 사용하여 카운트하던데 그마저도 저런식으로 사용하니 0xDDDDDDDD로 채워진 이미 초기화 된 변수가 나타났다. 좀처럼 이해하기 힘든 동작이였다.

수시간, 그리고 며칠동안 구글링 아무리 해봐도 shared_ptr는 참조 카운트가 스레드 안전하다고 할 뿐 좀 더 자세한 내용 잘 나오지도않고(…) 그냥 이렇게 사용해야 정말 안전하다 정도로 알고 넘어가야겠다 -_-;;;