iPhone上的延迟数字

每个程序员都应该知道的延迟数 -“每个程序员都应该知道的延迟”表。 它包含2012年执行基本计算机操作的时间平均值。 该表有多个替代视图,这里是其中之一。


链接到架构源

但是,2019年该信息对移动开发人员有何好处? 似乎没有,但是Yandex.Navigator团队的Dmitry KurkinSClown )认为:“现代iPhone的桌子会是什么样?” 结果出在Dmitry关于AppsConf的报告的修订文本中。


这是为了什么


程序员为什么要知道这些数字? 它们与移动开发人员相关吗? 这些数字可以解决两个主要任务。

了解计算机的时间尺度


一个简单的情况-电话交谈。 我们可以轻松地说出这个过程何时快速,何时漫长:几秒钟非常快,几分钟是一次平均对话,一个小时或更长的时间很长。 加载页面的过程类似:在不到一秒钟的时间内(很快,几秒钟)可以忍受,而一分钟就是灾难,用户可能不必等待下载。

但是,诸如将数字添加到数组这样的操作(人们有时喜欢在采访中谈论的非常“快速插入”)又如何呢? 智能手机需要多少钱? 纳秒,微秒还是毫秒? 我见过很少有人会说1毫秒很长的时间,但就我们而言,情况确实如此。

各种计算机组件的速度比


在各种设备上执行操作的时间可能相差数十倍或数百倍。 例如,对主存储器的访问时间与访问L1缓存的时间相差100倍。 这是一个很大的差异,但不是无限的。 如果我们对此有特定含义,则在优化应用程序时,我们可以评估是否会节省时间。



现实生活中的“延迟数”


当我看到这些数字时,我开始对缓存和内存访问之间的区别感兴趣。 如果我小心翼翼地将数据放入不小于64 KB的字节中,那么我的代码将以100倍的速度运行-速度很快,一切都会飞起来!



我立即想将其全部检查出来,展示给我的同事,并尽可能地应用它。 我决定从Apple提供的标准工具开始-XSTest和measureBlock。 该测试的组织方式如下:确定一个数组,将其填充数字,对其进行XOR'il运算,然后重复10次该算法。 之后,我查看了一个元素需要花费多少时间。

缓冲区大小总时间手术时间
50 kb1.5毫秒30纳秒
500 kb12毫秒24毫微秒
5000 kb85毫秒17 ns

缓冲区的大小增加了100倍,并且操作时间不仅没有增加100倍,而且减少了近2倍。 先生们,军官,他们出卖了我们?!

得出这样的结果之后,我大为怀疑,这些数字可以在现实生活中看到。 常规应用程序可能无法感觉到这种差异。 也许在移动平台上,一切都不同。

我开始寻找一种方法来查看缓存和主内存之间的性能差异。 在搜索过程中,我遇到了一篇文章,作者抱怨说,他在Mac和iPhone上运行了基准测试,没有显示这些延迟。 我拿了这个工具并得到了结果-就像在药房一样。 当缓冲区大小超过相应缓存的大小时,内存访问时间会明显增加。



LMbench帮助我获得了这些结果。 这是由Linux内核的开发者之一拉里·麦克沃伊(Larry McVoy)创建的基准,它使您能够测量内存访问时间,切换线程和文件系统操作的成本,甚至是主处理器操作所花费的时间:加,减等。德州仪器(TI)为处理器提供了有趣的测量数据 。 LMBench用C编写,因此在iOS上运行它并不困难。

内存成本


有了如此出色的工具,我决定进行类似的测量,但要针对实际的移动设备-iPhone。 主要测量是在5S上进行的,然后当其他设备落入我的手中时,我得到了结果。 因此,如果未指定设备,则为5S。

记忆体存取


对于此测试,使用一个特殊的数组,其中填充有互相引用的元素。 每个元素都是指向另一个元素的指针。 数组不是通过索引遍历的,而是从一个节点到另一个节点的转移。 这些元素散布在整个阵列中,因此,在访问新元素时,它通常尽可能不在缓存中,而是从RAM中卸载。 这种安排尽可能地干扰了缓存。

您已经看到了初步结果。 在L1高速缓存的情况下,它小于10纳秒,对于L2,则是几十纳秒,而在主存储器的情况下,时间增加到数百纳秒。



读写速度


测量了三个主要操作:

  • 阅读( p [i] + )-我们阅读元素并将它们添加到总量中;
  • record( p [i] = 1 )-在每个元素中写入一个常数;
  • 读写( p [i] = p [i] * 2 )-我们取出元素,对其进行更改,然后将新值写回。

使用缓冲区时,使用2种方法:在第一种情况下,仅使用每四个元素,而在第二种情况下,所有元素都按顺序使用。



以较小的缓冲区大小获得最高速度,然后根据L1和L2高速缓存的大小有明确的步骤。 最有趣的是,当顺序读取数据时,不会降低速度。 但是在通过的情况下,可以看到清晰的步骤。



在顺序读取期间,操作系统设法将必要的数据加载到缓存中,因此对于任何缓冲区大小,我都不需要访问内存-所有必需的数据都是从缓存中获取的。 这解释了为什么我在基本测试中没有看到时差。

读写操作的测量结果表明,在正常应用中,很难获得100倍的估计加速度。 一方面,系统本身很好地缓存了数据,即使使用大型数组,我们也很可能在缓存中找到数据。 另一方面,使用各种变量很容易需要访问内存,并且损失了数百纳秒。

L1L2记忆体
延迟数1纳秒7 ns100毫微秒
iPhone 5s7 ns30纳秒240毫微秒
iPhone 6s Plus5纳秒12纳秒200纳秒
iPhone X2纳秒12纳秒146 ns

穿线费用


接下来,我想获得与线程一起使用的类似数据,以便了解使用多线程的成本 :创建一个线程并从一个线程切换到另一个线程要花费多少钱。 对我们来说,这些都是频繁的操作,我想了解损失。

仪器。 系统跟踪


系统跟踪对跟踪应用程序中的线程工作很有帮助。 在WWDC 2016上对该工具进行了详细描述。 该工具有助于按流状态查看过渡,并在三个主要类别中显示流上的数据:系统调用,处理内存和流状态。



  1. 系统调用 它们以红色“香肠”的形式呈现。 指向它们时,您可以看到系统方法的名称和执行的持续时间。 通常在应用程序应用程序中,这样的系统调用不会直接发生:我们使用某些东西,而后者又已经调用了system方法。 您不应该依赖这样的事实,即您的代码中的方法在此处可见。
  2. 内存操作 。 它们以蓝色“香肠”的形式呈现。 这包括诸如内存分配,释放,清零等操作。
  3. 流的状态 。 蓝色-线程正在运行,某些处理器正在从该线程执行代码。 灰色-线程由于某种原因被阻塞,无法继续执行。 红色-线程已准备就绪,但是目前没有可用的内核来执行其代码。 橙色-中断流程以进行更高优先级的工作。
  4. 兴趣点 。 这些是特殊标签,可以通过调用kdebug_signpost由代码安排。 标签可以是单个标签(在代码中的特定位置),也可以是范围标签(以突出显示整个过程)。 使用这样的标签,将微秒和系统调用与您的应用程序关联起来要容易得多。

流创建成本


第一个测试是在新线程中执行任务 。 我们创建具有特定过程的线程,然后等待其完成工作。 将总时间与该过程本身的时间进行比较,我们得到在新线程中启动该过程的总损失。



在系统跟踪中,您可以清楚地看到所有事情是如何发生的:



  1. 创建流。
  2. 我们的过程在其中运行的新线程。 开头的红色区域表示该线程已创建,但由于没有可用的内核,因此一段时间以来无法执行。
  3. 流的完成。 有趣的是,线程完成过程本身甚至比其创建还要大。 尽管删除似乎总是更快。
  4. 等待该过程的完成,这是原始方案中的过程,并且在流结束之后结束-暂时,该方法意识到了这一点,然后报告。 此时间比流完成的时间略长。

因此,创建流需要相当大的成本:iPhone 5S-230微秒,6S-50微秒。 流的完成花费的时间几乎是创建时间的2倍 ,加入流也需要花费明显的时间。 当使用内存时,我们得到了数百纳秒,比数十微秒少100倍。

开销创造结束参加
iPhone 5s230微秒40微秒70微秒30微秒
iPhone 6s Plus50微秒12微秒20微秒7微秒

信号量切换时间


下一个测试是对信号量的工作进行测量 。 我们有2个预先创建的线程,每个线程都有一个信号量。 流交替发信号通知邻居的信号量,并等待它们的信号量。 彼此传递信号,小溪互相打乒乓球,相互复兴。 这种双重迭代使信号量切换时间加倍。



在系统跟踪中,一切看起来都类似:

  1. 给第二流的信号量信号。 可以看出该操作非常短。
  2. 第二个线程被解锁,对其信号量的等待结束。
  3. 为第一流的信号量提供信号。
  4. 第一个线程被解除阻塞,对其信号量的等待结束。



切换时间在10微秒以内。 创建线程50次的区别恰恰是创建线程池的原因,而不是每个过程都有一个新线程。

系统线程上下文切换的损失


在前面的两个测试中,线程之间的控制传递是完全受控的-我们清楚地了解了应该在哪里发生过渡。 但是,经常会发生系统本身从一个线程切换到另一个线程的情况。 当我们并行运行的任务多于设备内核时,操作系统必须能够切换自身以为每个人提供处理器时间。

在此测试中,我想衡量启动太多线程的损失。 为此,创建了一个由16个线程组成的池,每个线程都等待一个信号量,并在接收到信号后立即执行特定的过程并向该信号量发信号。 主线程启动整个池,给出16个信号,然后等待16个信号作为响应。



在系统跟踪中,您可以看到这些块是随机分散的,其中一些块的长度比其余块长得多。 如果多次切换导致操作的执行时间增加,那么平均执行时间将因此增加。


但是,随着线程数的增加,平均操作时间不会增加。

理论上,只要负载对应于处理能力,就应保持平均时间。 即,任务数量对应于核心数量。



如果并行运行许多任务,则OS从一项任务切换到另一项任务将引入更多延迟。 这应该反映在结果中。


实际上,不仅我们的应用程序可以在设备上运行,而且仍然具有许多并行和系统进程。 即使是我们应用程序中的唯一线程也将受到切换的影响,这会导致中断和延迟。 因此,在所有情况下都存在延迟,并且是串行构建任务还是并行运行任务都没有区别。



以下是我们的延迟编号表,其中包含有关流和信号量的数据。

L1L2记忆体信号量
延迟数1纳秒7 ns100毫微秒25 ns
iPhone 5s7 ns30纳秒240毫微秒8微秒
iPhone 6s Plus5纳秒12纳秒200纳秒5微秒
iPhone X2纳秒12纳秒146 ns3.2微秒

档案费用


我们已经有内存和线程-为了完整起见,我们只需要文件系统操作。

读取档案


第一个测试是读取速度 -读取文件要花多少钱。 该测试包括两个部分。 首先,我们考虑文件打开,读取和关闭的情况下测量读取速度 。 在第二个例子中 ,我们假设文件一直处于打开状态 :我们将自己放置在某个地方并读取所需的内容。

从两个角度正确查看了结果。 当文件较小时 ,从文件中读取数据的时间最少。 最长1 KB为5.3微秒-没关系:1字节,2或1 KB-整个5.3μs。 因此,您只能在文件较大的情况下谈论速度,而固定时间已经可以忽略。 对于任何文件大小,打开和关闭文件的操作大约需要相同的时间-在5S的情况下,大约需要50微秒。



为了读取速度,获得了这样的图。



对于iPhone X和1 MB的文件,速度可以达到20 MB / s。 有趣的是,读取1 MB的文件效率更高。 对于大文件,缓存大小似乎会受到影响。 这就是为什么速度会进一步下降并在10 Mb的范围内保持均匀的原因。

创建和删除文件


该测试包括以下步骤: 创建文件并写入数据 ,然后删除创建的文件。 结果是逐步的:在小尺寸上,时间稳定-大约7μs,并且还会进一步增长。 比例是对数的。



令我惊讶的是,删除大文件所需的时间与创建时间相当,因为我认为删除是一种快速的操作。 事实证明,对于iPhone而言,及时删除与创建文件相当。 摘要表如下所示。

L1L2记忆体信号量磁碟
延迟数1纳秒7 ns100毫微秒25 ns150微秒
iPhone 5s7 ns30纳秒240毫微秒8微秒5微秒
iPhone 6s Plus5纳秒12纳秒200纳秒5微秒4微秒
iPhone X2纳秒12纳秒146 ns3.2微秒1.3微秒

结论


根据这些测量,我们现在对基本的iOS操作需要多少时间有了一个想法:访问内存为纳秒,处理文件的时间为微秒,创建流的时间为数十微秒,而切换仅为几微秒。

要使应用程序真正挂起,程序的执行时间必须超过15毫秒(以60fps的速度更新屏幕所花费的时间)。 这几乎是本文中进行的大多数测量的一千倍。 在这样的规模上,毫秒是相当大的,一秒已经是“永远”。

测试表明,尽管访问内存和缓存的时间差异很大,但是直接使用此比率还是很困难的。 在根据L1编译所有数据之前,需要确保您的情况确实能得到结果。

根据使用线程进行的操作测试,我们能够确保创建和销毁线程需要大量时间,但是执行大量并行操作不会带来额外的成本。

好吧,总而言之,我想提醒您在进行性能测试时最重要的规则- 首先进行测量,然后进行优化

GitHub上的简介发言人Dmitry Kurkin。

将AppsConf 2018报告转换和转换为文章的过程筹备全新的 2019年会议同时进行 。 到目前为止, 已接受报告列表中只有7个主题,但是此列表将一直扩展,以便于4月22日至23日为移动开发人员举办一个很棒的会议。

关注出版物,订阅youtube频道新闻通讯 ,这一次很快就会过去。

Source: https://habr.com/ru/post/zh-CN437590/


All Articles