"Review your app's data design and update it to conform with 64-bit architecture."
앱의 데이터 디자인을 리뷰하고 64비트 아키텍처를 따르도록 업데이트합니다.
앱을 64비트 아키텍처로 옮길 때, 코드에서 구조를 다시 살펴봐야 합니다. 32비트 표현을 갖는 C 구조를 검사해야 합니다. 64비트 런타임은 구조의 컨텐츠를 다르게 정렬합니다. 그렇기 때문에 파일에 데이터를 저장하거나 네트워크를 통해 전송하는 구조에 신경써야 합니다. 이와 같은 구조는 다른 크기의 아키텍처를 갖는 기기에서 활용될 수 있습니다.
명시적으로 크기가 지정된 타입을 사용하지 않는 코드는 코드를 리뷰하는 다른 개발자에게 문제를 일으킬 수 있습니다. 명시적으로 크기가 지정된 타입의 선언은 데이터 크기 예상을 명확하게 하고, 아키텍처에 대한 가정을 제거합니다.
함수에서의 타입 선언과 코드에서의 해당 타입 사용 사이의 비일관성은 런타임에 일관적이지 않은 동작을 야기합니다. 함수에 전달하는 데이터 혹은 반환 값으로 캡처하는 데이터를 선언된 파라미터 타입과 같은 타입으로 확실히 해야 할 필요가 있습니다.
64비트의 증가된 크기의 결과로 런타임에서의 모든 64비트 인티저 타입 정렬이 4바이트에서 8바이트로 변경되었습니다. 각 인티저 타입을 명시적으로 구체화할지라도 두 구조는 런타임에서 동일하지 않을 것입니다. 만약 32비트 버전에서 생성되었던 데이터를 가지고 있는 경우 64비트 버전에서 읽을 필요가 있다는 것을 알아야 합니다.
아래 코드에서 정렬은 필드가 명시적으로 인티저 타입으로 선언되었다고 하더라도 변경됩니다.
struct bar {
int32_t foo0;
int32_t foo1;
int32_t foo2;
int64_t bar;
};
이 코드가 32비트 컴파일러에서 컴파일되면, 필드 바는 구조의 시작에서부터 12바이트로 시작합니다. 패딩의 4바이트는 바가 8바이트 영역에서 정렬되기 위해 foo2
후에 추가됩니다.
새 데이터 구조를 정의하려면, 가장 큰 값 정렬을 첫 번째로 두고 가장 작은 값을 마지막에 둠으로써 요소들을 조직화해야 합니다. 이 조직화는 대부분의 패딩 바이트에 대한 필요를 제거합니다. 잘못 정렬된 64비트 인티저를 포함하는 기존 구조에서 작업하고 잇는 경우 적합한 정렬에 강제시킬 수 있도록 프라그마를 사용할 수 있습니다. 아래 코드는 같은 데이터 구조를 보여주면서도 32비트 정렬 규칙 사용에 강제되고 있습니다.
#pragma pack(4)
struct bar {
int32_t foo0;
int32_t foo1;
int32_t foo2;
int64_t bar;
};
#pragma options align=reset
이 옵션은 아껴서 사용해야 합니다. 왜냐하면 잘못 정렬된 접근에 대한 성능 페널티가 있기 때문입니다. 예를 들어 앱의 32비트 버전에서 배포된 데이터 구조에 백워드 호환성을 유지하기 위해 이 옵션을 사용하게 될 것입니다.
C99 표준은 하드웨어 아키텍처 기반에 상관없이 특정 크기가 되도록 보장하는 내장된 데이터 타입을 제공합니다. 아래 테이블은 C99 타입을 리스트로 보여주고 있고, 각각에 대한 값의 범위를 정의하고 있습니다.
Table 1 Types and ranges
Type | Defined range |
---|---|
int16_t | -32,768 to 32,767 |
int32_t | -2,147,483,648 to 2,147,483,647 |
int64_t | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
uint8_t | 0 to 255 |
uint16_t | 0 to 65,535 |
uint32_t | 0 to 4,294,967,295 |
uint64_t | 0 to 18,446,744,073,709,551,615 |
short
, int
, unsigned int
와 같은 타입은 피하시기 바랍니다. 하드웨어 아키텍처 기반 상관없이 대신 특정 크기가 될 수 있도록 보장받는 타입을 사용하시기 바랍니다.
고정된 넓이 타입을 선택하는 것 또한 필요한 것보다 지나치게 큰 범위를 갖는 변수 할당을 피합니다. 그리고 메모리를 아낍니다.
코드에서 데이터 타입의 비일관적인 사용은 연산의 결과를 잘라낼 수 있고, 부정확한 결과를 제공할 수 있습니다. 컴파일러가 비일관적인 데이터 타입에서 나타나는 문제들을 경고할지라도 이러한 패턴의 다양한 것들을 보는 것은 유용합니다. 이렇게 함으로써 이들을 코드에서 파악할 수 있을 것입니다.
함수를 호출할 때, 하앙 결과로 받는 변수와 함수의 반환 타입을 일치시켜야 합니다. 만약 반환 타입이 받는 변수보다 더 큰 인티저라면, 값은 잘립니다. 아래 코드는 이 문제를 나타내는 간단한 패턴을 보여주고 있습니다.
long PerformCalculation(void);
int x = PerformCalculation(); // Incorrect.
long y = PerformCalculation(); // Correct.
PerformCalculation
함수는 긴 인티저를 반환합니다. 32비트 런타임에서 int
와 long
모두 32비트입니다. 그래서 코드가 부정확하더라도 int
타입에 대한 할당은 작동합니다. 64비트 런타임에서 할당이 만들어질 대 결과의 상위 32비트는 손실됩니다. 대신 결과는 긴 인티저에 할당되어야 합니다. 이 접근방식은 모든 런타임에서 일관적으로 작동합니다.
파라미터로써 값에 전달할 때 같은 문제가 발생합니다. 아래에 64비트 런타임에서 실행될 때 입력 파라미터가 잘리는 것을 보여주고 있습니다.
int PerformAnotherCalculation(int input);
long i = LONG_MAX;
int x = PerformCalculation(i);
아래 코드에서 반환 값은 64비트 런타임에서도 잘립니다. 왜냐하면 반환된 값은 함수 반환 타입의 범위를 초과하기 때문입니다.
int ReturnMax()
{
return LONG_MAX;
}
이러한 모든 예시는 int
및 long
이 동일하다고 가정하는 코드로부터 결과가 나타나게 합니다. ANSI C 표준은 이와 같은 가정을 하지 않으며, 64비트 런타임에서 작동할 때 명시적으로 부정확한 것입니다. 기본값으로 Updating Your App from 32-Bit to 64-Bit Architecture에서 설명하는 것처럼 프로젝트를 현대화한 경우 -Wshorten-64-to-32 컴파일러 옵션은 자동으로 활성화되었을 것이고, 그렇기에 컴파일로는 값이 잘린 많은 경우에 대해서 자동으로 경고할 것입니다. 선택적으로 더 자세하면서도 잠재적 에러를 찾을 수 있는 -Wconversion 옵션을 포함시키길 원할 수도 있습니다.
LLVM 컴파일러에서 열거된 타입은 열거형의 크기를 정의할 수 있습니다. 그렇기 때문에 몇 가지 열거된 타입은 기대한 것보다 클 수 있습니다. 다른 모든 경우와 마찬가지로 해결방법은 데이터 타입의 크기에 대해 가정을 갖지 않는 것입니다. 대신 모든 열거된 값을 적합한 데이터 타입을 갖는 변수에 할당하는 것입니다.
NSInteger
타입은 numeric 타입을 선언하기 위해 시스템을 통해 사용됩니다. 32비트런타임에서는 32비트 인티저이며, 64비트 런타임에서는 64비트 인티저입니다. NSInteger
타입이 int
타입과 같은 크기일 것이라고 가정하지 않아야 합니다. 아래에 몇 가지 사례가 있습니다.
NSNumber
객체로 혹은 객체로부터 변환합니다.NSCoder
클래스를 사용해서 데이터를 인코딩하고 디코딩합니다. 특히 64비트 기기에서 NSInterger
를 인코딩하고 이후에 32비트 기기에서 디코딩하는 경우, 만약 값이 32비트 인티저 범위를 초과하게 되면 디코딩 메소드는 예외를 반환할 것입니다. 대신 명시적 인티저 타입을 사용하길 원할 것입니다.NSInteger
로써 다룹니다. 특히 NSNotFound
상수는 다음과 같습니다. 64비트 런타임에서 이것의 값은 int
타입의 최대값 범위보다 더 큽니다. 그렇기에 앱에서 값을 자르는 것이 에러를 발생시킵니다.CGFloat
의 크기를 가정하지 않아야 합니다. 변환의 결과로 인해서 CGFloat
타입은 64비트 floating-point 숫자로 변경됩니다. NSInterger
타입처럼 CGFloat
이 float
혹은 double
이라고 가정할 수 없습니다. 그렇기 때문에 일관적으로 사용해야 합니다. 아래 코드는 CFNumber
를 생성하기 위해 코어 파운데이션을 사용하는 예시를 보여주고 있습니다.
// Incorrect.
CGFloat value = 200.0;
CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &value);
// Correct.
CGFloat value = 200.0;
CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &value);
코드의 첫 번째 파트는 CGFloat
이 float
과 같은 크기로 가정하고 있으며, 이는 부정확합니다. 코드의 두 번째 파트는 kCFNumberCGFloatType
을 생성하고자 하는 타입으로써 정확하게 구체화하고 있습니다.
코드에 있는 포인터가 64비트 런타임에서 안전하도록 보장합니다.
https://developer.apple.com/documentation/uikit/app_and_environment/updating_your_app_from_32-bit_to_64-bit_architecture/auditing_pointer_usage
https://velog.io/@panther222128/Auditing-Pointer-Usage
코드가 정확하게 함수, 함수 포인터, Objective-C 메시지를 처리할 수 있도록 보장합니다.
https://developer.apple.com/documentation/uikit/app_and_environment/updating_your_app_from_32-bit_to_64-bit_architecture/managing_functions_and_function_pointers
https://velog.io/@panther222128/Managing-Functions-and-Function-Pointers