Struktur vs. Kelas

Sejak awal, ketika saya mulai pemrograman, muncul pertanyaan tentang apa yang harus digunakan untuk meningkatkan kinerja: struktur atau kelas; array mana yang lebih baik untuk digunakan dan caranya. Mengenai struktur, Apple menyambut penggunaannya, menjelaskan bahwa mereka lebih baik dalam pengoptimalan, dan seluruh esensi bahasa Swift adalah struktur. Tetapi ada orang-orang yang tidak setuju dengan ini, karena Anda dapat dengan indah menyederhanakan kode dengan mewarisi satu kelas dari yang lain dan bekerja dengan kelas semacam itu. Untuk mempercepat kerja dengan kelas, kami membuat pengubah dan objek yang berbeda yang dioptimalkan khusus untuk kelas, dan sudah sulit untuk mengatakan apa yang akan lebih cepat dan dalam hal ini.

Untuk mengatur semua poin pada "e", saya menulis beberapa tes yang menggunakan pendekatan biasa untuk pemrosesan data: meneruskan ke suatu metode, menyalin, bekerja dengan array, dan sebagainya. Saya memutuskan untuk tidak membuat kesimpulan besar, semua orang akan memutuskan untuk dirinya sendiri apakah layak untuk mempercayai tes, akan dapat mengunduh proyek dan melihat bagaimana itu akan bekerja untuk Anda, dan mencoba mengoptimalkan pengoperasian tes tertentu. Mungkin bahkan chip baru akan keluar yang tidak saya sebutkan, atau mereka sangat jarang digunakan sehingga saya belum pernah mendengarnya.

PS Saya mulai mengerjakan sebuah artikel tentang Xcode 10.3 dan saya berpikir untuk mencoba membandingkan kecepatannya dengan Xcode 11, tapi tetap saja artikelnya bukan tentang membandingkan dua aplikasi, tetapi tentang kecepatan aplikasi kita. Saya tidak ragu bahwa runtime fungsi akan berkurang, dan apa yang telah dioptimalkan dengan buruk akan menjadi lebih cepat. Akibatnya, saya menunggu Swift 5.1 yang baru dan memutuskan untuk menguji hipotesis dalam praktik. Selamat membaca.

Tes 1: Bandingkan Array pada Struktur dan Kelas


Misalkan kita memiliki kelas, dan kita ingin meletakkan objek kelas ini ke dalam array, tindakan yang biasa dilakukan pada sebuah array adalah untuk mengulanginya.

Dalam sebuah array, ketika menggunakan kelas di dalamnya dan mencoba melewatinya, jumlah tautan bertambah, setelah selesai jumlah tautan ke objek akan berkurang.

Jika kita pergi melalui struktur, maka pada saat objek dipanggil oleh indeks, salinan objek akan dibuat, melihat area memori yang sama, tetapi ditandai tidak dapat diubah. Sulit untuk mengatakan apa yang lebih cepat: meningkatkan jumlah tautan ke suatu objek atau membuat tautan ke suatu area dalam memori dengan kurangnya kemampuan untuk mengubahnya. Mari kita periksa dalam praktik:


Fig. 1: Perbandingan mendapatkan variabel dari array berdasarkan pada struktur dan kelas

Tes 2. Bandingkan ContiguousArray vs Array


Yang lebih menarik adalah membandingkan kinerja array (Array) dengan array referensi (ContiguousArray), yang diperlukan secara khusus untuk bekerja dengan kelas yang disimpan dalam array.

Mari kita periksa kinerja untuk kasus-kasus berikut:

ContiguousArray menyimpan struct dengan tipe nilai
ContiguousArray menyimpan struct dengan String
ContiguousArray menyimpan kelas dengan tipe nilai
ContiguousArray menyimpan kelas dengan String
Array penyimpanan struct dengan tipe nilai
Array menyimpan struct dengan String
Array kelas penyimpanan dengan tipe nilai
Array menyimpan kelas dengan String

Karena hasil pengujian (tes: meneruskan ke fungsi dengan optimasi sebaris dimatikan, beralih ke fungsi dengan optimasi sebaris dihidupkan, menghapus elemen, menambahkan elemen, akses berurutan ke elemen dalam satu lingkaran) akan mencakup sejumlah besar tes (untuk 8 array masing-masing dari 5 tes masing-masing) , Saya akan memberikan hasil yang paling signifikan:

  1. Jika Anda memanggil fungsi dan meneruskan array ke dalamnya, mematikan inline, maka panggilan seperti itu akan sangat mahal (untuk kelas berdasarkan String referensi, 20.000 kali lebih lambat, untuk kelas berdasarkan Nilai, tipenya 60.000 kali, lebih buruk dengan pengoptimal inline dimatikan) .
  2. Jika optimisasi (sebaris) bekerja untuk Anda, maka degradasi diharapkan hanya 2 kali, tergantung pada tipe data apa yang ditambahkan ke array mana. Satu-satunya pengecualian adalah tipe nilai, yang dibungkus dalam struktur yang terletak di ContiguousArray - tanpa degradasi waktu.
  3. Penghapusan - penyebaran antara array referensi dan yang biasa sekitar 20% (mendukung Array biasa).
  4. Tambahkan - ketika menggunakan objek yang dibungkus dalam kelas, ContiguousArray memiliki kecepatan sekitar 20% lebih cepat daripada Array dengan objek yang sama, sementara Array lebih cepat ketika bekerja dengan struktur daripada ContiguousArray dengan struktur.
  5. Akses ke elemen array ketika menggunakan pembungkus dari struktur ternyata lebih cepat daripada pembungkus lainnya di kelas, termasuk ContiguousArray (sekitar 500 kali lebih cepat).

Dalam kebanyakan kasus, menggunakan array reguler untuk bekerja dengan objek lebih efisien. Digunakan sebelumnya, kami menggunakan lebih lanjut.

Optimalisasi loop untuk array dilayani oleh initializer koleksi lazy, yang memungkinkan Anda untuk berjalan hanya satu kali di seluruh array, bahkan jika beberapa filter atau peta digunakan di atas elemen array.

Dalam penggunaan struktur sebagai alat optimisasi, ada jebakan, seperti penggunaan tipe yang di dalamnya memiliki sifat referensi: string, kamus, array referensi. Kemudian, ketika variabel yang menyimpan tipe referensi sendiri adalah input ke fungsi, referensi tambahan dibuat untuk setiap elemen yang merupakan kelas. Ini memiliki sisi lain, sedikit lebih jauh. Anda bisa mencoba menggunakan kelas pembungkus di atas variabel. Kemudian jumlah tautan ketika meneruskan ke fungsi hanya akan meningkat untuk itu, dan jumlah tautan ke nilai-nilai di dalam struktur akan tetap sama. Secara umum, saya ingin melihat berapa banyak variabel dari tipe referensi harus dalam struktur sehingga kinerjanya menurun lebih rendah daripada kinerja kelas dengan parameter yang sama. Ada sebuah artikel di web yang disebut "Hentikan Penggunaan Structs!" Yang menanyakan pertanyaan yang sama dan menjawabnya. Saya mengunduh proyek dan memutuskan untuk mencari tahu apa yang terjadi di mana dan dalam kasus apa kita mendapatkan struktur yang lambat. Penulis menunjukkan kinerja struktur yang rendah dibandingkan dengan kelas, dengan alasan bahwa membuat objek baru jauh lebih lambat daripada meningkatkan referensi ke objek itu tidak masuk akal (jadi saya menghapus garis di mana objek baru dibuat dalam loop setiap kali). Tetapi jika kita tidak membuat tautan ke objek, tetapi hanya mengirimkannya ke fungsi untuk bekerja dengannya, maka perbedaan kinerja akan sangat tidak signifikan. Setiap kali kita menaruh sebaris (tidak pernah) pada suatu fungsi, aplikasi kita harus menjalankannya dan tidak membuat kode dalam sebuah string. Dilihat oleh tes, Apple membuatnya sehingga objek yang diteruskan ke fungsi sedikit dimodifikasi, untuk struktur kompiler mengubah mutabilitas dan membuat akses ke properti non-mutable dari objek malas. Sesuatu yang serupa terjadi di kelas, tetapi pada saat yang sama meningkatkan jumlah referensi ke objek. Dan sekarang kita memiliki objek malas, semua bidangnya juga malas, dan setiap kali kita memanggil variabel objek, ia menginisialisasinya. Dalam hal ini, struktur tidak memiliki yang sama: ketika suatu fungsi memanggil dua variabel, struktur objek hanya sedikit lebih rendah daripada kelas dalam kecepatan; ketika Anda memanggil tiga atau lebih, struktur akan selalu lebih cepat.

Uji 3: Bandingkan kinerja Struktur dan Kelas yang menyimpan kelas besar


Juga, saya sedikit mengubah metode itu sendiri, yang dipanggil ketika variabel lain ditambahkan (dengan cara ini, tiga variabel diinisialisasi dalam metode, dan bukan dua, seperti dalam artikel), dan bahwa tidak akan ada limpahan Int, saya mengganti operasi pada variabel dengan penjumlahan dan pengurangan. Menambahkan metrik waktu yang lebih jelas (dalam tangkapan layar itu adalah detik, tapi itu tidak begitu penting bagi kami, memahami proporsi yang dihasilkan adalah penting), menghapus kerangka kerja Darwin (saya tidak menggunakan dalam proyek, mungkin sia-sia, tidak ada perbedaan dalam tes sebelum / setelah menambahkan kerangka kerja), dimasukkannya optimalisasi maksimum dan build pada rilis build (tampaknya ini akan lebih jujur), dan inilah hasilnya:


Fig. 2: Kinerja struktur dan kelas dari artikel "Hentikan Penggunaan Structs"

Perbedaan dalam hasil tes dapat diabaikan.

Uji 4: Fungsi Menerima Generik, Protokol, dan Fungsi Tanpa Generik


Jika kita mengambil fungsi generik dan melewatkan dua nilai di sana, disatukan hanya dengan kemampuan untuk membandingkan nilai-nilai ini (func min), maka kode tiga baris akan berubah menjadi kode delapan (seperti kata Apple). Tapi ini tidak selalu terjadi, Xcode memiliki metode optimasi di mana, jika, ketika memanggil fungsi, ia melihat bahwa dua nilai struktural diteruskan ke sana, secara otomatis menghasilkan fungsi yang mengambil dua struktur dan tidak menyalin nilai-nilai lagi.


Fig. 3: Fungsi Generik Khas

Saya memutuskan untuk menguji dua fungsi: yang pertama, tipe data Generik dideklarasikan, yang kedua menerima hanya Protokol. Dalam versi baru Swift 5.1 Protocol itu bahkan sedikit lebih cepat daripada Generic (sebelum Swift 5.1 protokolnya 2 kali lebih lambat), meskipun menurut Apple itu seharusnya sebaliknya, tetapi ketika harus melewati array, kita sudah perlu mengetik, yang sudah melambat, Umum (tetapi masih bagus, karena lebih cepat dari protokol):


Fig. 4: Perbandingan fungsi host Generik dan Protokol.

Tes 5: Bandingkan panggilan metode induk dan yang asli, dan pada saat yang sama periksa kelas terakhir untuk panggilan seperti itu


Yang selalu membuat saya tertarik adalah seberapa lambat kelas bekerja dengan sejumlah besar orang tua, seberapa cepat suatu kelas memanggil fungsinya dan fungsi orang tua. Dalam kasus di mana kami mencoba memanggil metode yang mengambil kelas, pengiriman dinamis mulai berlaku. Apa ini Setiap kali suatu metode atau variabel dipanggil di dalam fungsi kita, sebuah pesan dihasilkan menanyakan objek untuk variabel atau metode ini. Objek, menerima permintaan seperti itu, memulai pencarian metode dalam tabel pengiriman kelasnya, dan jika override metode atau variabel dipanggil, ia akan mengambil dan kembali, atau secara rekursif mencapai kelas dasar.


Fig. 5: Panggilan metode kelas, untuk pengujian pengiriman

Beberapa kesimpulan dapat diambil dari tes di atas: semakin besar kelas kelas induk, semakin lambat akan bekerja, dan bahwa perbedaan dalam kecepatan sangat kecil sehingga dapat diabaikan dengan aman, kemungkinan besar optimasi kode akan membuatnya sehingga tidak akan ada perbedaan dalam kecepatan. Dalam contoh ini, pengubah kelas akhir tidak memiliki keuntungan, sebaliknya, pekerjaan kelas bahkan lebih lambat, mungkin karena fakta bahwa itu tidak menjadi fungsi yang sangat cepat.

Tes 6: Memanggil variabel dengan pengubah akhir terhadap variabel kelas reguler


Juga hasil yang sangat menarik dengan menetapkan pengubah akhir ke variabel, Anda dapat menggunakannya ketika Anda tahu pasti bahwa variabel tidak akan ditulis ulang di mana saja di ahli waris kelas. Mari kita coba untuk menempatkan pengubah akhir ke variabel. Jika dalam pengujian kami, kami hanya membuat satu variabel dan memanggil properti di atasnya, maka itu akan diinisialisasi satu kali (hasilnya dari bawah). Jika kita secara jujur ​​membuat setiap kali objek baru dan meminta variabelnya, kecepatannya akan terasa melambat (hasilnya di atas):


Fig. 6: Panggil variabel terakhir

Jelas, pengubah tidak pergi untuk kepentingan variabel, dan selalu lebih lambat dari pesaingnya.

Tes 7: Masalah polimorfisme dan protokol untuk struktur. Atau kinerja wadah Eksistensial


Masalah: jika kita mengambil protokol yang mendukung metode tertentu dan beberapa struktur yang diwarisi dari protokol ini, apa yang akan dipikirkan oleh kompiler ketika kita meletakkan struktur dengan volume yang berbeda dari nilai yang disimpan dalam satu array, disatukan oleh protokol asli?

Untuk mengatasi masalah memanggil metode yang telah ditentukan dalam ahli waris, mekanisme Tabel Protokol Saksi digunakan. Ini menciptakan struktur shell yang merujuk pada metode yang diperlukan.

Untuk mengatasi masalah penyimpanan data, wadah Eksistensial digunakan. Ini menyimpan sendiri 5 sel informasi, masing-masing 8 byte. Di tiga yang pertama, ruang dialokasikan untuk data yang disimpan dalam struktur (jika mereka tidak cocok, maka itu menciptakan tautan ke tumpukan di mana data disimpan), yang keempat menyimpan informasi tentang jenis data yang digunakan dalam struktur, dan memberi tahu kami cara mengelola data ini , yang kelima berisi referensi ke metode objek.


Gambar 7. Perbandingan kinerja array yang membuat tautan ke objek dan yang berisi itu

Antara hasil pertama dan kedua, jumlah variabel naik tiga kali lipat. Secara teori, mereka harus ditempatkan dalam wadah, mereka disimpan dalam wadah ini, dan perbedaan dalam kecepatan adalah karena volume struktur. Menariknya, jika Anda mengurangi jumlah variabel dalam struktur kedua, waktu operasi tidak akan berubah, yaitu, wadah sebenarnya menyimpan 3 atau 2 variabel, tetapi tampaknya ada kondisi khusus untuk satu variabel yang secara signifikan meningkatkan kecepatan. Struktur kedua sangat cocok dengan wadah dan berbeda dalam volume dari yang ketiga oleh setengah, yang memberikan degradasi yang kuat dalam runtime, dibandingkan dengan struktur lainnya.

Sedikit teori untuk mengoptimalkan proyek Anda


Faktor-faktor berikut dapat mempengaruhi kinerja struktur:

  • tempat variabel disimpan (heap / stack);
  • perlunya penghitungan referensi untuk properti;
  • metode penjadwalan (statis / dinamis);
  • Copy-On-Write hanya digunakan oleh struktur data yang merupakan tipe referensi yang berpura-pura menjadi struktur (String, Array, Set, Dictionary) di bawah tenda.

Pantas untuk segera mengklarifikasi bahwa yang tercepat dari semuanya adalah benda-benda yang menyimpan properti dalam tumpukan, jangan gunakan penghitungan referensi dengan metode statis pemeriksaan medis.

Kelas lebih buruk dan berbahaya dibandingkan dengan struktur



Kami tidak selalu mengontrol penyalinan objek kami, dan jika kami melakukan ini, kami bisa mendapatkan terlalu banyak salinan yang akan sulit dikelola (kami membuat objek dalam proyek yang bertanggung jawab untuk membentuk tampilan, misalnya).

Mereka tidak secepat struktur.

Jika kita memiliki tautan ke suatu objek dan kita mencoba mengendalikan aplikasi kita dalam gaya multi-ulir, kita bisa mendapatkan Kondisi Balapan ketika objek kita digunakan dari dua tempat yang berbeda (dan ini tidak begitu sulit, karena proyek yang dibangun dengan Xcode selalu sedikit lebih lambat, dari versi Toko).

Jika kita mencoba menghindari Kondisi Balapan, kita menghabiskan banyak sumber daya pada Lock dan data kita, yang mulai memakan sumber daya dan membuang waktu alih-alih pemrosesan cepat dan kita bahkan mendapatkan objek yang lebih lambat daripada yang sama yang dibangun di atas struktur.

Jika kita melakukan semua tindakan di atas pada objek kita (tautan), maka kemungkinan kebuntuan yang tidak terduga itu tinggi.

Kompleksitas kode meningkat karena ini.

Lebih banyak kode = lebih banyak bug, selalu!

Kesimpulan


Saya pikir kesimpulan dalam artikel ini hanya perlu, karena saya tidak ingin membaca artikel dari waktu ke waktu, dan daftar poin yang terkonsolidasi sangat diperlukan. Merangkum garis-garis di bawah tes, saya ingin menyoroti yang berikut ini:

  1. Array paling baik ditempatkan dalam array.
  2. Jika Anda ingin membuat array dari kelas, lebih baik memilih Array biasa, karena ContiguousArray jarang memberikan keuntungan, dan mereka tidak terlalu tinggi.
  3. Optimasi inline mempercepat kerja, jangan mematikannya.
  4. Akses ke elemen Array selalu lebih cepat daripada akses ke elemen ContiguousArray.
  5. Struktur selalu lebih cepat daripada kelas (kecuali tentu saja Anda mengaktifkan optimasi modul Utuh atau optimasi serupa).
  6. Ketika mengirimkan objek ke fungsi dan memanggil propertinya, mulai dari yang ketiga, struktur lebih cepat dari kelas.
  7. Saat Anda meneruskan nilai ke fungsi yang ditulis untuk Generic dan Protocol, Generic akan lebih cepat.
  8. Dengan multiple class inheritance, kecepatan pemanggilan fungsi menurun.
  9. Variabel menandai pekerjaan akhir lebih lambat dari paprika biasa.
  10. Jika suatu fungsi menerima suatu objek yang menggabungkan beberapa objek dengan protokol, maka ia akan bekerja dengan cepat jika hanya satu properti yang disimpan di dalamnya, dan akan sangat menurun ketika menambahkan lebih banyak properti.

Referensi:
medium.com/@vhart/protocols-generics-and-existential-contain-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
Kode sumber tes

Source: https://habr.com/ru/post/id472290/


All Articles