Android内存性能

最近看测试同学在上班时 “原神-启动!”,以为是蹭公司网更新下新的纸片人老婆,后来才知道是在模拟Android低内存场景,看是否能杀死测试机上后台某个Service。

没有趁手的工具,怎么能顺利测试,遂着手开始写一个内存测试工具,起名Ramtool。

1.最大可用物理内存

1.1 背景

如何占用Android的所有物理内存,并不是直接写个java类不停通过new申请内存就可以了。

众所周知,Android的Framework在java层,所以一个Android App进程必须要启动一个jvm虚拟机(dalvik或者art),而运行在jvm中的所有程序,都会受到jvm最大heap限制。例如大家日程使用的eclipse,IDEA,Android Studio等

Android的jvm也不例外,这个逻辑是由system/build.prop控制的,现代的Android系统默认jvm启动heap为64MB,最大扩容256MB。如果你在AndroidManifest.xml中声明android:largeHeap="true",最大容量可以达到512MB;可即使是512MB,也离现在的Android的物理内存上限相差甚远,最新的Android设备已经有24GB的内存了。

1.2 方案

1.2.1 占用内存

总体上就是要摆脱android jvm。android内核为Linux,使用C/C++制作Native动态链接库,直接向Linux内核申请内存。

不仅如此,Native上还可以摆脱jvm的操作时内存的cpu损耗,摆脱jvm的gc控制,实现手动释放内存防止影响后续测试。

就行了吗?

现代的操作系统,包括Linux,都是用了虚拟内存的概念来进行进程间内存隔离。每个进程都有自己的内存地址起始,例如进程A的第一个内存地址是0x0000A,进程B的第一个内存地址也是0x0000A。在内核上会把进程的虚拟内存地址映射到真实的物理内存地址上。

如果只是不停通过malloc申请堆内存,就会发现程序在耗光内存最大寻址空间前(理论上限:32位CPU是4GB,64位CPU是2097152 TB,实际上还要受到具体CPU硬件实现的限制)即使申请的内存容量已经远远超过硬件物理内存,也不会发生OOM崩溃。

所以

申请到的内存要真正填入数据才能被操作系统映射到物理内存上。我这里使用memset + 填入rand生成随机数占用内存

1.2.2 工程实现

众所周知Native OOM崩溃可能会影响进程稳定性,申请到的内存释放也会非常麻烦,所以,放到子进程里去申请heap内存,每次申请1MB,直到子进程崩溃,catch住binder的跨进程通信异常时,就判断为可用的最大物理内存。

就可以了吗?

在大部分机型上都是没问题的,但在部分超大内存机型上,例如realme的16GB内存机型,单进程系统限制最大内存为10GB(因为每次测试结果都是恰好10240MB),所以为了测试极限物理内存占用,还要采用多进程方案。

申请完内存,记得要在每次测试完成后,即使进行内存释放。内存泄漏会影响后续内存测试的准确性。

1.2.3 结果

6GB ram的红米K30手机上,app最大可用的物理内存极限在4GB左右

16GB ram的Realme手机上,app最大可用的物理内存极限在11.5GB左右

2.内存带宽

2.1背景

做完最大可用内存后,感觉做都做了,不如再做一个内存带宽测试工具。市面上的内存带宽测试都太抽象了,不知道对应到代码上具体是什么样的性能。

内存不像是硬盘,可以很直接看到读写性能(直接将内存生成的数据写入硬盘文件),内存很难直接测试读写;而且同样的内存,读写性能很大程度上,也会受到CPU性能影响,尤其是CPU的内存控制器。

所以应该怎样做呢?
 

2.2方案

2.2.1 开源方案

先看下开源的方案都是怎么做的。例如比较知名的弗吉尼亚大学研发的stream:非常简单粗暴,例如copy算法中,复制一个变量,对于内存来说就是一读一写,所以对一个数组,进行循环拷贝,统计拷贝过的数据量直接x2,就是stream算法中的copy的带宽;其他算法以此类推,只是将复制换成了赋值、乘法加法等操作。

2.2.2 硬件原理

我们再来看下CPU的硬件工作方式,CPU都是通过内存总线访问内存,单个内存总线时钟周期可以获取 内存带宽位 的数据,例如常见的x86 pc上,单通道内存位宽为64bit,那么单个内存总线时钟可以获取64bit也即是8byte,8个字节的数据。

为了最大程度利用内存带宽,所以stream中选择数据类型也使用了double(因为在任何机型上,C语言中的double都是64位)

32平台长度(bytes)

64位平台长度(bytes)

char

1

1

short int

2

2

int

4

4

long int

4

8

long long int

8

8

long

4

8

long long

8

8

float

4

4

double

8

8

size_t

4

8

ssize_t

4

8


 

所以直接用stream就完了吗

Stream的方案并不能最大程度发挥内存带宽。

2.2.3 小故事

(此段故事可以跳过阅读)还要从我几个月前写的游戏说起:几个月前我把之前任天堂FC/NES游戏主机上的一款RPG游戏移植到Android平台(有人可能会说你是不是闲的,直接用模拟器玩不行吗?我的回答是不行,我就是闲的,要你寡),其中图形引擎上,我使用了大量的内存操作(例如将预渲染的地图,拷贝到opengl显示用的buffer上),发现在一台旧手机上不是很流畅。其实这台手机的理论性能并不是很差,CPU为64位的arm-v8a(是的,不要怀疑,不是软件限制,还有32位的arm-v8,64位的arm-v8叫做aarch64,感兴趣32位arm-v8是什么奇葩内核可以去翻一下arm官网文档,arm-v8还分好几个小版本),内存拷贝应该不会很慢

(故事结束,正文开始)但实际上内存copy就是慢的一批,测试发现系统memcpy很慢,开始翻对应Android版本的Linux内核源代码,发现历史上Linux的内存拷贝可以简单理解为就是个for循环,按照最小内存单位(8bit,1字节)进行拷贝...

且现代处理器都支持SIMD指令(就是单指令,多数据。什么年代了还在用传统SISD指令(丁真.jpg)你说你用Java?你用JavaScript?),例如x86指令集阵营intel 和 AMD的CPU上古时代就支持的SSE系列指令集,AVX指令集;arm阵营的neon指令,advanc-simd指令,还有最新arm-v9上的SVE指令,都是SIMD指令集。使用SIMD指令对于测试内存带宽,可以最大限度避免CPU的影响,提高带宽。

2.2.4 最终方案

参考ARM公司最新的优化库,分段对内存进行拷贝:对于对齐的大块数据(>128字节),使用SIMD指令,直接单次指令拷贝128字节;较大数据(>64字节),使用64位寄存器单次拷贝;依此类推。使用SIMD指令的memcpy进行内存带宽测试,相比于stream的内存copy方案,内存带宽大很多。

其他内存操作算法参考stream。

内存带宽测试功能截图:copy(legacy)为循环拷贝,使用了4指令循环展开优化;copy(arm-simd)为使用simd指令进行优化的内存拷贝

3.缓存/内存延迟

3.1 背景

做完内存带宽后,感觉做都做了(梅开二度),专业测试工具怎么能少了内存延迟测试。

CPU只能操作寄存器上的数据,CPU在操作数据前必须要将数据从内存载入到寄存器。但内存太慢了,所以CPU厂商通过SRAM实现了缓存,SRAM很快但容量小价格巨贵,所以只能用来做内存与CPU之间

常用数据的中转站。缓存对于程序来说是透明的(我调研了x86_64和arm-v8指令集,都没有发现任何能直接操作缓存的指令),只能通过细微的逐步扩大容量访问内存才能测得缓存延迟。

所以应该怎样做呢?

3.2 方案

3.2.1算法

先看看开源是怎么做的吧。Larry McVoyCarl Staelin 制作的lmbench,已经被unbuntu官方收录,应该还是很权威的。lmbench源代码中,用于测试延迟的类都是lat开头的,其中内存延迟测试的源代码为lat_mem_rd,其中核心代码为:

#define    ONE            p = (char **)*p;

#define    FIVE    ONE ONE ONE ONE ONE

#define    TEN            FIVE FIVE

#define    FIFTY    TEN TEN TEN TEN TEN

#define    HUNDRED    FIFTY FIFTY



    while (iterations-- > 0) {

        for (i = 0; i < count; ++i) {

            HUNDRED;

        }

    }

通过指针循环向后访问上一个内存地址(后面会解释),除以访问次数得到每次内存寻址的延迟(当然这个结果也不严格意义上的硬件内存延迟,因为指针也是个内存变量,还要读取和赋值...目前业界所有的软件都无法测试直接的硬件延迟,包括aida64,只能说无限接近)

为什么要向后访问?因为现代的CPU都有硬件预取功能,CPU的分支预测也依赖于内存预取。但大部分CPU只能向前预取内存,不能向后预取,使用向后取内存地址,可以最大限度防止内存预取对测试结果的影响。

3.2.2参数

lmbench的算法可以直接借鉴来用!那么如何制订合适的lmbench参数呢(步长 和 测试容量):

  1. 步长设置过低会导致 再大测试容量也无法测得正确的内存延迟,过大会导致过慢,数据量过多

  2. 测试容量设置过低 会导致 无法测得完整延迟数据“未到达内存延迟平台”,过大会导致测试过慢,数据量过多

  3. L1 ~ L3 cache~内存,容量指数增长,扩容算法需为非线性扩容

经过多次实验(4GB ~ 16GB)机型测试,步长取2048字节,容量为256MB时,测试结果较为准确,且测试时长可接受。

3.2.3 数据处理

经过上面的步骤,已经可以成功取到内存延迟的数据

 

将数据导入excel,先画图观察下数据曲线

初步数据曲线

首先访问命中cache,随着访问容量增大,逐渐冲破cache容量,cache命中率下降。可以发现数据特征:多段式,阶段间线性增长(缓存命中率逐渐降低),存在数据尖刺(波动)

算法实现:
  • 我们需要的只是各个阶段的 “平台” 数据(就是图中三段持平的数据),所以可以简单通过sort,先去除尖刺,sort后数组记为latency

  • 计算latency的(n) - (n-1)差值,记为差值数组diff

  • 然后统计diff数组<1的连续区间,这个区间就是图像中的 “平台”。区间大小记为count(就是右端点减左端点),对应的latency区间的平均值计为avgLatency,存储到platform数组

  • 对platform数组,按照count进行排序(count就是可信度),取前4条,其中avgLatency最小的可以认为是L1 cache的延迟,最大的是memory的延迟

对延迟数据sort前后的图像对比

计算结果前4日志:platform0代表存在一段近似持平了36个x轴单位的直线数据,平均延迟为112ns(注意x轴单位非线性,遵循lmbench延迟扩容算法)

3.2.4 结果

通过数据分析,计算出cache和内存的延迟平台的均值大小,还用数据画了个折线图:

4.总结:

似乎目前市面上Android平台还没有比较专业倾向的内存测试工具,简陋封装了下UI,发布到了github,让大家有的用。

测试结果仅具备横向机型间对比;不同算法、不同参数、不同软件的测试结果不具备可比性。

Ramtool项目地址:GitHub - park671/Ramtool: Android专业内存测试工具:内存带宽(包括SIMD指令集内存带宽),cache + 内存延迟,最大可用内存

拒绝白嫖,从你我做起,给个star再拿代码~

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值