您的位置:首页 > 运维架构 > 网站架构

32位架构应用转64位架构小结

2017-05-01 13:14 113 查看

64位应用适配

简介

同桌面系统架构一样,苹果公司从 iOS 7 开始采用64位的A7处理器。在相同的设备上,运行相同的应用,支持64位的应用比支持32位的应用性能更高。

苹果公司的A7处理器支持两个不同的指令集,一个32位的 ARM 指令集,这是为了支持以前的处理器,另一个便是全新的64位的 ARM 指令集。这个全新的64位架构,不仅拥有更大的地址空间,而且其全新的线性指令集更是支持两倍于以前的整型及浮点型寄存器。为了更好的利用64位处理器的性能,苹果公司优化了 LLVM 编译器,这样,64位应用能够更好的处理数据,而对于64位整数运算及 NEON 操作的性能提升更加明显。所以,尽管32位应用在A7处理器上运行比在以前的处理器上运行更快,但是,将其转换为64位应用后性能更佳。

当64位的应用运行在 iOS 上时,其指针及一些原本是32位长度的数据类型也会变为64位长度,这意味着更多内存的消耗,所以,若不谨慎处之,内存消耗可能抵消由32位转64位所带来的性能提高。

当 iOS 运行在64位机上时,其包含有32位与64位两个不同版本的系统框架,但是减少系统内存的消耗,更快的加载应用,当设备上运行的应用均为64位时,iOS 并不会将32位版本的系统框架加载至内存。所以,应用支持64位设备对大家均有好处。

Xcode 构建应用时,所生成的二进制机器码是默认包含了32位和64位的,但这种混合构建的要求是,部署目标的 iOS 版本不低于 5.1.1 ,而其中64位的二进制机器码只能在64位设备上执行,并且该设备搭载的 iOS 系统版本必须是 7.0.3 或其之后。所以,对于已经存在的应用,应先升级到 iOS 7 或 iOS 7 之后,而后再在64位机上运行,对于新建应用,目标版本应为 iOS 7 或其之后并编译32位和64位两个版本。

iOS 应用于 OS X 应用架构大部分相同,一些通用的代码可以很容易的运行在两个系统上,同样,64位的升级过程也有诸多相似之处。其中指针和一些 C 语言基本类型从32位变为64位,与 NSIntegerCGFloat 相关的代码需要仔细检查。

确定所有函数调用有相应的原型声明

避免64位变量被意外赋32位类型而遭截断

确定在64位中的运行准确

创建数据结构时,应保证其32位与64位两个版本的结构一致

对于32位升级至64位的应用,应用虚拟机测试两个版本均正常,最后发布前,用64位设备测试正常后,才可发布。

64位架构的主要变化

对数据类型的影响

为了使不同的代码集成后能够正常运行,这些代码便需要遵循一些约定,其中包括基本数据类型的大小及格式,就如同一个代码片段中调用其他代码时使用的指令一样,而编译器便是基于这些约定实现的,故不同代码编译成二进制后,能够集成在一起正常执行。而这些约定可参见 application binary interface (ABI)

iOS 应用依赖底层应用接口与系统架构,从 iOS 7 开始,一些 iOS 设备使用64位处理器,并提供32位及64位的运行环境。而对于大多的应用,64位运行环境与32位运行环境有两个重要的不同:

在64位运行环境中,Cocoa Touch 框架使用的大多数据类型同 Objective-C 语言本身的数据类型一样,他们的数据长度都增长了,并且有严格的内存对齐规则。

在64位运行环境中,任何函数的调用,都需要相应的函数原型声明

在 C 与 Objective-C 语言中,数据类型的大小及其在内存中的对齐方式并不是预定义好的,相反,每个平台定义其构建的数据类型的长度。这就意味着,只要遵循语言标准中的严格定义,平台就可以使得变量最大程度的适配底层硬件和操作系统。iOS 上的64位运行环境,改变了内部数据类型的大小,同样,在更高层的 Cocoa Touch 架构中使用的数据类型长度也随之改变。32位与64位系统有约定的名称,分别为
ILP32
LP64
,并且编译器在为64系统编译代码时,会定义
__LP64__
宏变量。

32位与64位架构中的变量大小及对齐的长度差别如下表所示:



在 iOS 与 OS X 中,32位与64位并不影响浮点型数据的大小,他们在内存中的对齐方式也始终是自然对齐,但是对于 CGFloat 类型由 float(4 bytes) 类型变为了 double(8 bytes) 类型,这就使得 Quartz 架构及其他使用了 Core Graphics 类型的架构中的变量表示范围更大,也更准确。另外,64位的 ARM 架构环境使用小端字节序,这与支持 ARMv7 架构的 ARM 处理器使用的32位 iOS 运行环境相同。

数据类型长度的变化对应用的影响也是不容忽视的,在编译同一个应用代码到32位与64位两种不同的运行环境时,应注意这之间带来的性能及兼容问题。

由于数据类型的不同而引起的内存压力不同

32位应用与64位应用数据交换问题

两种架构中,运算结果的偏差

当长度长的数据类型的变量值赋值给了长度短的数据类型变量时,值会被截断,这可能导致赋值前后值不相同

对方法调用的影响

对于方法调用,若不涉及汇编语言,这些约定并不重要,但是在64位架构中,有时候最好清楚方法的调用过程。当方法被调用时,编译器会将参数传递给被调用者,这里,大多约定,相较于固定参数个数的方法,接收可变个参数的方法会被执行。每个方法必须有相应的原型声明,方法转换为不同版本,必须与原方法有相同的方法签名。

编写底层代码直接操控 Objective-C 的运行时的时候,再也不能直接方法对象的 isa 指针了,而是通过运行时方法获取相关信息。

因为32位与64位的指令集有很大不同,所以,若应用中包含有汇编语言代码,则需要使用新的指令集重写该部分代码。当然,因为64位架构的ARM的方法调用习惯与ARM标准并不完全一致,详细理解应参考 iOS ABI Function Call Guide

升级应用到64位

要支持64位 ARM 架构,iOS 的最低版本不能低于5.1.1,并且应在 build setting 中的 Architectures 选项中选择 Standard architectures(armv7,arm64) 选项,而后修改更新代码,使其兼容两个架构,最后测试并发布。

修改更新代码时,应注意以下几点:

不应将指针类型转换为 int 类型

为了便于运算,通常会有下面的写法

int *c = something passed in as an argument....
int *d = (int *)((int)c + 4); // 错误写法
int *d = c + 1;               // 正确写法

在32位架构中,因为指针长度与 int 长度大小一样,所以不会出错,但是在64位架构中,
指针长度比 int 长度长了4个字节,所以这种写法会导致指针地址遭截断丢失。
若非要进行指针转整型,可使用 uintptr_t 类型替代。


保证使用的变量类型始终相同

如以下代码写法

long PerformCalculation(void);

int  x = PerformCalculation(); // 不正确
long y = PerformCalculation(); // 正确

当函数的返回值变量类型长度大于接收的变量类型长度时,会导致返回值的高位被截断,
从而导致结果错误,虽然这种写法在32机上不会出错,但是在64位机上却会出错,
而在 ANSI C 语言标准中并没有 int 与 long 类型长度一致的规定,
所以最好保持返回值类型与接收值得变量类型一致。
通常,在工程的编译选项中 -Wshorten-64-to-32 选项
都是有效的(可在 build setting 中搜索,如下图),
若想发现更多潜在错误,可将选项 -Wconversion 选项置为有效。在编程中,
注意 long ,NSInteger ,CFIndex ,size_t 等类型的正确使用,
而且注意 fpos_t 与 off_t 类型的长度始终是64位的,不要给他们赋 int 类型的值。
另外,枚举类型同样可以定义枚举变量大小,所以其大小可能与预想的不一样,
故不应假定其数据的长度,而是在赋值时,将其赋值给合适的数据类型变量。




Cocoa Touch 中常用类型的变化

在 Cocoa Touch 中,尤其是 Core Foundation 和 Foundation 中,
常常获取并使用 C 语言的变量及 Objective-C 的对象中的变量。
从32位架构到64位架构,NSInteger 的长度也从32位变为了64位,
所以不能认为 NSInteger 与 int 始终等长,在接收 NSInteger 类型的返回值时,
应使用 NSInteger 类型的变量。例如,在使用 NSNumber 保存或获取数据时,
使用 NSCoder 编码或解码数据时,均需注意不同架构带来的影响。
另外,对于使用 NSInteger 定义的常量值,如 NSNotFound 在不同架构中,
表示的值不相同,要注意因此导致的错误。
CGFloat 在64位架构中,长度也变为64位,
所以不能将其简单的等同于 float 或 double 类型,
使用时要保持一致。如下错误与正确用法的对比:
// 错误
CGFloat value = 200.0;
CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &value);

// 正确
CGFloat value = 200.0;
CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &value);


C 及 C 的派生语言的符号位扩展规则

C 语言及其相似语言,有一系列规则决定,当该变量被赋值给长度更大的变量时,变量的最高位(符号位)如何扩展,这些规则如下:

1. 当无符号位变量扩展时,扩展位补零即可
2. 有符号位变量扩展时,扩展位与符号位的值相同,即使扩展后的结果为无符号位变量
3. 常量(除了被前缀修改的,如 0x8L)的数据类型通常是能够表示该常量的最短长度的数据类型。
16进制表示的数据,可以是 int ,long ,long long 类型,可以是有符号或无符号类型。
10进制数据总是被当做有符号类型数据。
4. 等长度的有符号位数与无符号位数相加的结果是无符号位数


由于位扩展的规则,整数计算时,应注意32位机与64位机上运行结果的不同,如下面的例子:

int a = -2;
unsigned int b = 1;
long c = a + b;
long long d = c;


这个代码在32位架构上运行后,结果是-1(0xFFFFFFFF),但是,在64位机上结果却是4294967295(0x00000000FFFFFFFF),其运算过程如下:

计算机中的运算是以数据补码相加的形式进行的
32位架构运算执行过程:
a 的补码是 0xFFFFFFFE
b 的补码是 0x000000001
a + b = 0xFFFFFFFF
得到结果的补码形式,最高位为1,则结果是负值,
将补码取反码再加1,得到原码,为0x00000001,可知最终结果为-1

64位架构运算执行过程:
a 的补码是 0xFFFFFFFE
b 的补码是 0x00000001
a + b = 0xFFFFFFFF
得到结果的补码形式,最高位虽为1,但在64位架构中,int/unsigned int 为32位长度,
long 为64位长度,所以要进行位扩展,根据扩展规则第4条,这个结果被当做无符号数进行扩展,
所以扩展后结果为0x00000000FFFFFFFF,最高位为0,
则结果是正值,补码即为原码,最终结果为4294967295。
为兼容两种架构,可以在a与b相加之前,将b转换为 long 类型变量(0x0000000000000001)。
这样,在64位架构中,不同长度类型的数值相加,短长度的类型会扩展为长度长的类型,
所以执行加法操作前,a 会先扩展为 long 类型,即0xFFFFFFFFFFFFFFFE,
与0x0000000000000001相加,得到结果0xFFFFFFFFFFFFFFFF,即-1。


再比如下面的例子:

unsigned short a = 1;
unsigned long b = (a << 31);
unsigned long long c = b;

在32位架构中,结果正常为0x80000000,可是64位架构中结果为0xffffffff80000000,
这是因为 short 类型在执行移位操作之前会先转换为 int 类型,而且是有符号数,
故将0x80000000赋值给64位的b时,会进行符号位扩展(如扩展规则第二条)。
为了避免错误,应在移位操作之前,将a转换为 long 类型。


另外,不应认为数据类型的长度是不变的,在必要情况下,使用反码可避免一些错误。

function_name(long value)
{
long mask = ~0x3; // 0xfffffffc or 0xfffffffffffffffc
return (value & mask);
}


使用明确长度的数据类型及对齐方式

当需要在不同架构的机器上保存,操作文件或数据时,应保证数据的一致性。在C99标准中,定义了固定长度的变量类型,而不必在意底层硬件架构的区别。当我们自定义数据结构时,若必须确保该数据结构的长度是固定不变的,则本着不浪费内存的原则,选择使用固定长度的数据类型。C99标准中明确范围的整数类型如下表:



在64位运行环境中,64位长度的变量类型的对齐方式从4个字节变为8个字节,所以即使使用固定长度类型的变量,定义的数据结构在不同的运行环境中,也可能有不一样的长度。如下面的数据结构:

struct bar {
int32_t foo0;
int32_t foo1;
int32_t foo2;
int64_t bar;
};


在32位架构中,因为这4个变量均是按4个字节对齐,前3个变量共计12个字节,所以变量 bar 则从距离数据结构起始地址12个字节的位置开始存储,所以数据结构的长度为20个字节。但是在64位编译器的编译下,前3个变量按3个字节对齐,仍然不变,而变量 bar 则按照8个字节方式对齐,所以,编译器会在变量 foo2 的后面补4个字节,所以变量 bar 从距离数据结构起始地址的16个字节处开始存储,所以数据结构的长度为24个字节。由此可见,相同的数据结构,并且是指定了类型长度的变量,最终在不同的架构上因为对齐字节长度的不一致,而导致了数据结构长度的差别。

为了较少内存的消耗,在声明结构体内的变量时,通常将数据类型按对齐字节数从大到小依次声明,这样做可以尽可能的减少因对齐而进行的字节填充。当然,也可以使用 pragma 命令改变对齐字节数,如下:

#pragma pack(4)
struct bar { int32_t foo0; int32_t foo1; int32_t foo2; int64_t bar; };#pragma options align=reset


但是这种非自然对齐的方式,对性能影响较大,所以若非为兼容32位系统等必要情况,不宜使用。另外,使用 malloc 分配变量内存时,不应该传具体内存大小,而是应使用 sizeof 计算变量类型所占内存的大小。

支持两种运行环境的占位符

对于类似 printf 的函数,由于数据类型的改变,使得事情变得繁杂,但是为了支持不同的运行环境,可以使用在头文件 inttypes.h 中定义的宏变量。

如下表,分别列出了标准占位符与新增的占位符



如下面的例程,给出了打印同指针大小范围的整型变量与指针的方法

#include <inttypes.h>
void *foo;
intptr_t k = (intptr_t) foo;
void *ptr = &k;

printf("The value of k is %" PRIdPTR "\n", k);
printf("The value of ptr is %p\n", ptr);

打印结果类似于下
The value of k is 106102872259872
The value of ptr is 0x7fff5275cd80


注意函数及函数指针

对于32位与64位运行环境,其函数调用的处理方式是不同的,最大的区别是,有任意个参数的函数读取参数时的命令是不同于拥有固定个参数的函数的。所以,要确保函数调用的正确性,让其可以准确的找到其参数。为了使编译器确定函数的参数个数是否是固定的,每个函数必须有函数的原型声明。另外,函数指针的使用必须与函数原型保持一致,不能将指向固定个参数函数的函数指针转换为任意个参数函数的函数指针,反之亦不行。

但是也有例外,在 Objective-C 的运行时中,使用 objc_msgSend 等类似的函数处理消息时,不必遵循上述的规则,因为 Objective-C 的运行时是直接转至方法的实现处,但是需要将 objc_msgSend 函数转换与方法一致的函数声明,这里注意方法的函数声明在编译时,会自动插入两个参数,这两个参数是 id 与 SEL ,分别是类实例及方法选择器。

在使用任意个参数的函数时,注意其参数是不会自动转化的,如函数接收的参数类型是 long 而入参是 int 类型,那么,其会接着读取下一个参数的内容,相反,函数的参数类型是 int 而提供了 long 类型,则参数的剩余部分会传入下一个参数,这无疑会出错。

优化内存性能

64位编译器的改进,使得64位应用比32位应用运行性能更好,但同时,这种改进增大了指针及数据所占内存的大小,这反过来影响了应用的性能,所以开发64位应用,一定要全面优化内存的使用。在优化前,先比较应用在32位与64位环境中运行的内存变化,哪里的内存消耗有明显增加,就优化哪里的内存使用。

常见的内存使用问题

在 Foundation 等框架中提供了很多方便的特性,但他们的使用会消耗内存,如使用 NSDictionary 单独存储一对键值对比直接使用变量存储要消耗更多内存,所以应避免这种低负载的使用方式。

压缩数据的存储,如使用多个变量存储日期的年月日,不如存储相较于某一个时间点的秒数,使用时再计算出具体时间

struct date
{
NSInteger second;
NSInteger minute;
NSInteger hour;
NSInteger day;
NSInteger month;
NSInteger year;
};

上面这种方式存储时间,会占用24个字节,在64位环境中更是多达48个字节,
不如使用 ANSI C 所定义的 time_t 类型存储秒数,但是注意其长度会因环境不同而改变。

struct date
{
time_t seconds;
};


在数据结构中,将对齐字节数长的变量类型靠前声明,尽量减少因对齐而填充的字节数

尽量减少指针的使用,提高数据的负载

struct node
{
node        *previous;
node        *next;
uint32_t    value;
};

这种结构,在64位环境中,内存的80%被指针使用,
应尽量避免,而是使用数组或其他集合类型保存指针索引值。


使用 malloc 方法申请内存时,申请较大内存比单独为每一个数据结构申请内存更有效,避免系统因对齐而填充字节

必要时才使用缓存

使用缓存可以提高性能,但是太依赖缓存,造成虚拟内存系统压力过大反而会降低性能,所以以下情况应避免使用缓存:

能够轻易计算出来的数据

能够轻易从其他类中获取的数据或对象

能够轻松重新生成的系统对象

能够通过 mmap() 访问的只读数据
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  arm ios 64位 32位 架构