结构与类

从一开始,当我开始编程时,就出现了使用什么来提高性能的问题:结构或类;结构或类。 哪种数组更好,如何使用。 关于结构,Apple欢迎使用它们,并解释说它们在优化方面更胜一筹,而Swift语言的全部本质就是结构。 但是有些人不同意这一点,因为您可以通过从另一个类继承另一个类并使用此类来精美地简化代码。 为了加快使用类的速度,我们创建了专门针对类进行优化的不同修饰符和对象,在这种情况下,要说出哪种更快和更快速已经是很难了。

为了安排“ e”上的所有点,我编写了一些测试,这些测试使用常规的数据处理方法:传递给方法,复制,使用数组等。 我决定不作任何大的结论,每个人都将自己决定是否值得相信测试,能够下载该项目并查看它如何为您工作,并尝试优化特定测试的操作。 也许会出现我没有提到的新芯片,或者它们很少使用,以至于我还没有听说过。

PS我开始撰写有关Xcode 10.3的文章,我曾想过尝试将其速度与Xcode 11进行比较,但仍然该文章不是关于比较两个应用程序,而是关于我们应用程序的速度。 我毫不怀疑,函数的运行时间会减少,而优化不佳的函数会变得更快。 结果,我等了新的Swift 5.1,并决定在实践中检验这些假设。 阅读愉快。

测试1:比较结构和类上的数组


假设我们有一个类,并且要将此类的对象放入数组中,则对数组的通常操作是遍历它。

在数组中,当在其中使用类并尝试遍历该数组时,链接数增加,完成后,指向该对象的链接数将减少。

如果我们遍历该结构,那么在通过索引调用对象时,将创建该对象的副本,并查看相同的内存区域,但将其标记为不可变的。 很难说什么更快:增加到对象的链接数量或在缺乏更改对象能力的情况下在内存中创建指向区域的链接。 让我们在实践中检查一下:


1:基于结构和类从数组获取变量的比较

测试2.比较ContiguousArray与Array


更有趣的是将数组(Array)的性能与参考数组(ContiguousArray)进行比较,这对于处理数组中存储的类特别需要。

让我们检查以下情况的性能:

ContiguousArray存储值类型的结构
ContiguousArray使用字符串存储结构
ContiguousArray存储具有值类型的类
ContiguousArray使用String存储类
具有值类型的数组存储结构
带有字符串的数组存储结构
具有值类型的数组存储类
使用String的数组存储类

由于测试结果(测试:在关闭内联优化的情况下传递给函数,在打开内联优化的情况下传递给函数,删除元素,添加元素,对循环中的元素进行顺序访问)将包含大量测试(对于8个数组,每个数组包含5个测试) ,我将给出最重要的结果:

  1. 如果您调用一个函数并将一个数组传递给它,然后关闭内联,则这样的调用将非常昂贵(对于基于引用String的类,它的速度要慢20,000倍,对于基于Value的类,该类型的速度是60,000倍,并且在关闭内联优化器的情况下更糟) 。
  2. 如果优化(内联)对您有效,则应仅将其降级2次,具体取决于将哪种数据类型添加到哪个阵列。 唯一的例外是值类型,该值类型包装在ContiguousArray中的结构中-没有时间下降。
  3. 去除-参考阵列与常用阵列之间的差异约为20%(有利于常用阵列)。
  4. 追加-当使用包装在类中的对象时,ContiguousArray的速度比具有相同对象的Array快20%,而使用结构时,Array的速度比使用结构的ContiguousArray快。
  5. 使用结构中的包装器时,访问数组元素的速度比包括ContiguousArray在内的所有类包装器的访问速度都快(约快500倍)。

在大多数情况下,使用常规数组处理对象会更有效。 以前用过,我们再用。

数组的循环优化由惰性集合初始化程序提供,即使您在数组的元素上使用了多个过滤器或映射,也可以只对整个数组进行一次遍历。

在使用结构作为优化工具时,存在一些陷阱,例如使用本质上内部引用的类型:字符串,字典,引用数组。 然后,当将本身存储引用类型的变量输入到函数时,将为作为类的每个元素创建一个附加引用。 这还有另一面,关于它还有一点。 您可以尝试在变量上使用包装器类。 这样,传递给函数时的链接数将仅为此增加,并且指向结构内部值的链接数将保持不变。 通常,我想查看结构中必须有多少个引用类型的变量,以使其性能下降的幅度低于具有相同参数的类的性能。 Web上有一篇名为“停止使用结构!”的文章,提出了同样的问题并予以回答。 我下载了该项目,并决定弄清楚在什么地方发生什么情况以及在什么情况下我们得到缓慢的结构。 作者展示了与类相比结构的性能低下,认为创建新对象比增加对对象的引用要慢得多是荒谬的(因此,我删除了每次在循环中创建新对象的行)。 但是,如果我们不创建指向该对象的链接,而只是将其传递给一个函数来使用它,那么性能上的差异将是微不足道的。 每次我们将内联 (从不)放在一个函数上时,我们的应用程序都必须执行它,而不是在字符串中创建代码。 从测试的角度来看,Apple做出了一些修改,以便对传递给该函数的对象进行略微的修改,以使编译器更改结构的可变性并使对对象的非可变属性的访问变得惰性。 类中发生了类似的事情,但是同时增加了对该对象的引用数量。 现在我们有了一个惰性对象,它的所有字段也都是惰性的,并且每次我们调用一个对象变量时,它都会对其进行初始化。 在这种情况下,结构是不相等的:当一个函数调用两个变量时,对象的结构在速度上仅次于类; 当您调用三个或更多时,结构将始终更快。

测试3:比较存储大型类的结构和类的性能


我还稍微改变了方法本身,在添加另一个变量时调用了该方法(通过这种方式,在方法中初始化了三个变量,而不是在本文中初始化了两个变量),并且不会出现Int溢出,我将变量上的运算替换为和和减法。 添加了更清晰的时间指标(在屏幕截图中以秒为单位,但对我们而言并不那么重要,了解所产生的比例很重要),删除了Darwin框架(我没有在项目中使用,可能徒劳,添加框架之前/之后的测试没有差异),包括最大程度的优化和发布版本的构建(看来这会更诚实),结果如下:


2:“停止使用结构”一文中的结构和类的性能

测试结果的差异可以忽略不计。

测试4:接受泛型,协议和不泛型的函数


如果我们采用一个泛型函数并在其中传递两个值(仅通过比较这些值的能力(func min)来组合),则三行代码将变成八行代码(如Apple所说)。 但这并非总是如此,Xcode具有优化方法,当调用一个函数时,如果看到两个结构值被传递给它,Xcode会自动生成一个具有两个结构并且不再复制值的函数。


3:典型的通用功能

我决定测试两个函数:在第一个函数中,声明了Generic数据类型,第二个函数仅接受Protocol。 在新版本的Swift 5.1协议中,它甚至比Generic快一点(在Swift 5.1之前,协议要慢2倍),尽管根据Apple的说法应该是相反的,但是在传递数组时,我们已经需要键入,这会减慢速度通用的(但它们仍然很棒,因为它们比协议要快):


4:通用和协议主机功能的比较。

测试5:比较父方法和本机方法的调用,并同时检查最终类的调用


一直令我感兴趣的是,与许多父母一起上课的速度有多慢,一个班级调用它的功能以及父母的功能有多快。 在我们试图调用采用类的方法的情况下,动态调度就起作用了。 这是什么 每次在函数内部调用方法或变量时,都会生成一条消息,询问对象该变量或方法。 接收到此类请求的对象开始在其类的分发表中搜索该方法,如果调用了该方法或变量的替代,则将其接受并返回,或者递归地到达基类。


5:类方法调用,用于调度测试

从上面的测试可以得出几个结论:父类的类越大,它的工作就越慢,并且速度的差异是如此之小以至于可以安全地忽略它,最有可能的代码优化将使它变得没有速度上的差异。 在此示例中,最终类修饰符没有优势,相反,该类的工作甚至更慢,这可能是由于该类没有变为真正快速的函数。

测试6:针对常规类变量调用带有final修饰符的变量


将final修饰符分配给变量也是非常有趣的结果,当您确定该变量不会在类的继承人中的任何地方重写时,可以使用它。 让我们尝试将final修饰符放入变量。 如果在测试中我们仅创建了一个变量并在其上调用了一个属性,则该变量将被初始化一次(结果来自下面)。 如果我们诚实地每次创建一个新对象并请求其变量,速度将明显降低(结果如上):


6:调用最终变量

显然,修饰符并没有为变量带来好处,它总是比其竞争对手慢。

测试7:多态性和结构协议问题。 或现有容器的性能


问题:如果我们采用支持某种方法的协议以及从该协议继承的几种结构,那么当我们将具有不同存储值量的结构放在一个数组中并与原始协议结合在一起时,编译器会怎么想?

为了解决调用继承人中预定义的方法的问题,使用了协议见证表机制。 它创建引用必要方法的外壳结构。

为了解决数据存储的问题,使用了现有容器。 它本身存储5个信息单元,每个信息单元8个字节。 在前三个中,为结构中的已存储数据分配了空间(如果它们不适合,则会创建一个指向存储数据的堆的链接),第四个存储有关该结构中使用的数据类型的信息,并告诉我们如何管理此数据,第五部分包含对对象方法的引用。


图7.创建指向对象并包含对象的链接的数组的性能比较

在第一结果和第二结果之间,变量数量增加了两倍。 从理论上讲,它们应放置在一个容器中,并存储在该容器中,并且速度的差异是由于结构的体积而引起的。 有趣的是,如果减少第二个结构中的变量数量,则操作时间不会改变,也就是说,容器实际上存储了3个或2个变量,但是显然,对于一个变量,存在特殊条件,可以显着提高速度。 第二种结构完全适合容器,并且与第三种结构的体积相差一半,与其他结构相比,运行时间大大降低。

一些理论来优化您的项目


以下因素会影响结构的性能:

  • 它的变量存储在哪里(堆/堆栈);
  • 需要对属性进行引用计数;
  • 调度方法(静态/动态);
  • 写时复制仅由那些假装为内部结构(字符串,数组,集合,字典)的引用类型的数据结构使用。

值得立即说明的是,最快的将是那些将属性存储在堆栈中的对象,不要将引用计数与静态医学检查方法一起使用。

与结构相比,班级是坏的和危险的



我们并不总是控制对象的复制,如果这样做,我们将获得太多难以管理的副本(例如,我们在项目中创建了负责形成视图的对象)。

它们不如结构快。

如果我们有一个对象的链接并且我们试图以多线程的方式控制我们的应用程序,那么当我们在两个不同的地方使用我们的对象时,我们可以得到竞态条件(这并不那么困难,因为使用Xcode构建的项目总是有点慢,比商店版本)。

如果我们试图避免出现“竞争状况”,我们会在Lock和我们的数据上花费大量资源,这开始吞噬资源并浪费时间,而不是快速处理,因此,与构建在结构上的对象相比,我们得到的对象甚至更慢。

如果我们对对象(链接)执行上述所有操作,则无法预料的死锁可能性很高。

因此,代码复杂度在增加。

总是有更多的代码=更多的错误!

结论


我认为,本文中的结论仅是必要的,因为我不想不时阅读该文章,而合并要点列表仅是必要的。 总结一下测试中的内容,我想强调以下几点:

  1. 数组最好放在数组中。
  2. 如果要从类创建数组,则最好选择一个常规数组,因为ContiguousArray很少提供优点,而且它们也不是很高。
  3. 内联优化可加快工作速度,请不要将其关闭。
  4. 访问Array元素总是比访问ContiguousArray元素快。
  5. 结构总是比类快(除非您启用“整个模块优化”或类似的优化)。
  6. 从第三个对象开始,将对象传递给函数并调用其属性时,结构要比类快。
  7. 当您将值传递给为通用和协议编写的函数时,通用会更快。
  8. 使用多个类继承,函数调用的速度会降低。
  9. 变量标记最终工作的速度比常规胡椒慢。
  10. 如果一个函数接受一个将多个对象与协议结合在一起的对象,则仅在其中存储一个属性的情况下它将迅速运行,并且在添加更多属性时会大大降低性能。

参考文献:
medium.com/@vhart/protocols-generics-and-existential-containers-wait-what-e2e698262ab1
developer.apple.com/videos/play/wwdc2016/416
developer.apple.com/videos/play/wwdc2015/409
developer.apple.com/videos/play/wwdc2016/419
medium.com/commencis/stop-using-structs-e1be9a86376f
测试源代码

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


All Articles