Terlambat datang ke Q&A ini dengan jawaban yang sudah bagus, tapi saya ingin mengganggu karena orang asing terbiasa melihat hal-hal dari sudut pandang bit dan byte tingkat bawah dalam memori.
Saya sangat senang dengan desain yang tidak berubah, bahkan datang dari perspektif C, dan dari perspektif menemukan cara baru untuk secara efektif memprogram perangkat keras yang sangat buruk yang kita miliki saat ini.
Lebih lambat / lebih cepat
Mengenai pertanyaan apakah hal itu membuat segalanya lebih lambat, jawaban robot akan yes
. Pada tingkat konseptual yang sangat teknis seperti ini, kekekalan hanya bisa membuat segalanya lebih lambat. Perangkat keras berfungsi paling baik ketika tidak mengalokasikan memori secara sporadis dan hanya dapat memodifikasi memori yang ada (mengapa kita memiliki konsep seperti temporal locality).
Namun jawaban praktisnya adalah maybe
. Kinerja sebagian besar masih merupakan metrik produktivitas dalam basis kode non-sepele. Kami biasanya tidak menemukan basis kode mengerikan untuk mempertahankan kondisi ras sebagai yang paling efisien, bahkan jika kami mengabaikan bug. Efisiensi sering kali merupakan fungsi keanggunan dan kesederhanaan. Puncak optimasi mikro agak dapat bertentangan, tetapi biasanya disediakan untuk bagian kode terkecil dan paling kritis.
Mengubah Bit dan Byte yang Tidak Berubah
Berasal dari sudut pandang tingkat rendah, jika kita menggunakan konsep x-ray seperti objects
dan strings
dan sebagainya, intinya hanyalah bit dan byte dalam berbagai bentuk memori dengan berbagai karakteristik kecepatan / ukuran (kecepatan dan ukuran perangkat keras memori biasanya menjadi saling eksklusif).
Hirarki memori komputer menyukainya ketika kita berulang kali mengakses potongan memori yang sama, seperti pada diagram di atas, karena ia akan menjaga potongan memori yang sering diakses itu dalam bentuk memori tercepat (L1 cache, misalnya, yang hampir secepat register). Kami mungkin berulang kali mengakses memori yang sama persis (menggunakannya kembali beberapa kali) atau berulang kali mengakses bagian chunk yang berbeda (misal: perulangan elemen-elemen dalam chunk yang berdekatan yang berulang kali mengakses berbagai bagian dari chunk of memory itu).
Kami akhirnya melemparkan kunci pas dalam proses itu jika memodifikasi memori ini akhirnya ingin membuat blok memori yang sama sekali baru di samping, seperti:
... dalam hal ini, mengakses blok memori baru bisa memerlukan kesalahan halaman wajib dan kesalahan cache untuk memindahkannya kembali ke bentuk memori tercepat (sampai pada register). Itu bisa menjadi pembunuh kinerja nyata.
Ada beberapa cara untuk mengurangi ini, bagaimanapun, dengan menggunakan cadangan memori yang sudah dialokasikan sebelumnya, sudah tersentuh.
Agregat Besar
Masalah konseptual lain yang muncul dari pandangan tingkat yang sedikit lebih tinggi adalah hanya melakukan salinan agregat yang sangat besar dalam jumlah besar yang tidak perlu.
Untuk menghindari diagram yang terlalu rumit, mari kita bayangkan blok memori sederhana ini entah bagaimana mahal (mungkin UTF-32 karakter pada perangkat keras yang luar biasa terbatas).
Dalam hal ini, jika kita ingin mengganti "BANTUAN" dengan "KILL" dan blok memori ini tidak dapat diubah, kita harus membuat blok baru secara keseluruhan untuk membuat objek baru yang unik, meskipun hanya sebagian saja yang telah berubah :
Membentangkan imajinasi kita sedikit, salinan mendalam dari segala hal lain hanya untuk membuat satu bagian kecil menjadi unik mungkin cukup mahal (dalam kasus dunia nyata, blok memori ini akan jauh, jauh lebih besar untuk menimbulkan masalah).
Namun, terlepas dari biaya seperti itu, desain semacam ini akan cenderung jauh lebih rentan terhadap kesalahan manusia. Siapa pun yang telah bekerja dalam bahasa fungsional dengan fungsi murni mungkin dapat menghargai ini, dan terutama dalam kasus multithread di mana kita dapat melakukan multithread kode seperti itu tanpa peduli di dunia. Secara umum, programmer manusia cenderung tersandung perubahan negara, terutama yang menyebabkan efek samping eksternal untuk menyatakan di luar lingkup fungsi saat ini. Bahkan memulihkan dari kesalahan eksternal (pengecualian) dalam kasus seperti itu bisa sangat sulit dengan perubahan keadaan eksternal yang bisa berubah dalam campuran.
Salah satu cara untuk mengurangi pekerjaan penyalinan yang berlebihan ini adalah dengan membuat blok memori ini menjadi kumpulan pointer (atau referensi) ke karakter, seperti:
Maaf, saya gagal menyadari kita tidak perlu membuat L
unik saat membuat diagram.
Biru menunjukkan data yang disalin dangkal.
... sayangnya, ini akan menjadi sangat mahal untuk membayar biaya penunjuk / referensi per karakter. Selain itu, kami dapat menyebarkan konten karakter di seluruh ruang alamat dan pada akhirnya membayarnya dalam bentuk kapal yang berisi kesalahan halaman dan cache yang hilang, dengan mudah membuat solusi ini lebih buruk daripada menyalin seluruh hal secara keseluruhan.
Bahkan jika kita berhati-hati untuk mengalokasikan karakter ini secara bersamaan, katakanlah mesin dapat memuat 8 karakter dan 8 pointer ke karakter ke dalam garis cache. Kami akhirnya memuat memori seperti ini untuk melintasi string baru:
Dalam hal ini, kita pada akhirnya membutuhkan 7 baris cache berbeda dari memori yang berdekatan untuk dimuat untuk melintasi string ini, ketika idealnya kita hanya membutuhkan 3.
Memotong Data
Untuk mengurangi masalah di atas, kita dapat menerapkan strategi dasar yang sama tetapi pada tingkat 8 karakter yang lebih kasar, misalnya
Hasilnya membutuhkan 4 baris cache senilai data (1 untuk 3 pointer, dan 3 untuk karakter) untuk dimuat untuk melintasi string ini yang hanya 1 pendek dari optimum teoritis.
Jadi itu tidak buruk sama sekali. Ada beberapa pemborosan memori tetapi memori berlimpah dan menggunakan lebih banyak tidak memperlambat segalanya jika memori tambahan hanya akan menjadi data dingin tidak sering diakses. Ini hanya untuk data yang panas dan berdekatan di mana penggunaan dan kecepatan memori yang berkurang sering berjalan seiring di mana kami ingin memasukkan lebih banyak memori ke dalam satu halaman atau garis cache dan mengakses semuanya sebelum penggusuran. Representasi ini cukup ramah terhadap cache.
Kecepatan
Jadi menggunakan representasi seperti di atas dapat memberikan keseimbangan kinerja yang cukup baik. Mungkin penggunaan struktur data abadi yang paling kritis terhadap kinerja akan mengubah sifat potongan data yang tebal dan membuatnya unik dalam prosesnya, sementara menyalin bagian yang tidak dimodifikasi dengan dangkal. Ini juga menyiratkan beberapa overhead dari operasi atom untuk referensi potongan dangkal disalin dengan aman dalam konteks multithreaded (mungkin dengan beberapa penghitungan referensi atom terjadi).
Namun selama data yang tebal ini diwakili pada tingkat yang cukup kasar, banyak overhead ini berkurang dan bahkan mungkin diremehkan, sementara masih memberi kita keamanan dan kemudahan pengkodean dan multithreading lebih banyak fungsi dalam bentuk murni tanpa sisi eksternal efek.
Menyimpan Data Baru dan Lama
Di mana saya melihat kekekalan sebagai berpotensi paling membantu dari sudut pandang kinerja (dalam arti praktis) adalah ketika kita dapat tergoda untuk membuat seluruh salinan data besar untuk menjadikannya unik dalam konteks yang bisa berubah di mana tujuannya adalah untuk menghasilkan sesuatu yang baru dari sesuatu yang sudah ada dengan cara di mana kita ingin tetap baru dan lama, ketika kita bisa membuat potongan-potongan kecil itu unik dengan desain abadi yang hati-hati.
Contoh: Undo System
Contohnya adalah sistem undo. Kami mungkin mengubah sebagian kecil dari struktur data dan ingin mempertahankan bentuk asli yang dapat kami batalkan, dan bentuk yang baru. Dengan jenis desain permanen yang hanya membuat bagian struktur data yang kecil dan dimodifikasi menjadi unik, kami dapat menyimpan salinan data lama dalam entri undo dan hanya membayar biaya memori dari data bagian unik yang ditambahkan. Ini memberikan keseimbangan produktivitas yang sangat efektif (menjadikan penerapan sistem undo menjadi bagian dari kue) dan kinerja.
Antarmuka Tingkat Tinggi
Namun sesuatu yang aneh muncul dengan kasus di atas. Dalam konteks fungsi lokal, data yang bisa berubah-ubah sering kali paling mudah dan paling mudah untuk dimodifikasi. Lagi pula, cara termudah untuk memodifikasi array adalah dengan hanya memutarnya dan memodifikasi satu elemen pada satu waktu. Kita dapat akhirnya meningkatkan overhead intelektual jika kita memiliki sejumlah besar algoritma tingkat tinggi untuk memilih untuk mengubah array dan harus memilih yang sesuai untuk memastikan bahwa semua salinan dangkal tebal ini dibuat sementara bagian-bagian yang dimodifikasi adalah dibuat unik.
Mungkin cara termudah dalam kasus tersebut adalah dengan menggunakan buffer yang bisa berubah secara lokal di dalam konteks fungsi (di mana mereka biasanya tidak membuat kita tersandung) yang melakukan perubahan secara atomis ke struktur data untuk mendapatkan salinan yang tidak dapat diubah baru (saya percaya beberapa bahasa memanggil "transien" ini) ...
... atau kita mungkin hanya memodelkan fungsi transformasi tingkat yang lebih tinggi dan lebih tinggi di atas data sehingga kita dapat menyembunyikan proses memodifikasi buffer yang bisa berubah dan memasukkannya ke struktur tanpa melibatkan logika yang bisa diubah. Bagaimanapun, ini belum merupakan wilayah yang dieksplorasi secara luas, dan kami memiliki pekerjaan kami memotong jika kita merangkul desain yang lebih abadi untuk menghasilkan antarmuka yang bermakna untuk bagaimana mengubah struktur data ini.
Struktur data
Hal lain yang muncul di sini adalah bahwa ketidakmampuan yang digunakan dalam konteks kinerja-kritis mungkin akan ingin struktur data dipecah menjadi data yang tebal di mana potongan tidak terlalu kecil tetapi juga tidak terlalu besar.
Daftar tertaut mungkin ingin mengubah sedikit untuk mengakomodasi ini dan berubah menjadi daftar yang belum dibuka. Besar, array berdekatan mungkin berubah menjadi array pointer menjadi potongan berdekatan dengan pengindeksan modulo untuk akses acak.
Ini berpotensi mengubah cara kita melihat struktur data dengan cara yang menarik, sambil mendorong fungsi modifikasi dari struktur data ini agar menyerupai sifat yang lebih besar untuk menyembunyikan kompleksitas ekstra dalam menyalin beberapa bit di sini dan membuat bit lainnya unik di sana.
Performa
Ngomong-ngomong, ini adalah pandangan kecil saya yang lebih rendah tentang topik itu. Secara teoritis, ketidakmampuan dapat memiliki biaya mulai dari yang sangat besar hingga yang lebih kecil. Tetapi pendekatan yang sangat teoritis tidak selalu membuat aplikasi berjalan cepat. Mungkin membuat mereka terukur, tetapi kecepatan dunia nyata sering membutuhkan merangkul pola pikir yang lebih praktis.
Dari perspektif praktis, kualitas seperti kinerja, pemeliharaan dan keamanan cenderung berubah menjadi satu kekaburan besar, terutama untuk basis kode yang sangat besar. Sementara kinerja dalam arti absolut terdegradasi dengan kekekalan, sulit untuk memperdebatkan manfaatnya terhadap produktivitas dan keselamatan (termasuk keselamatan benang). Dengan peningkatan ini sering kali terjadi peningkatan kinerja praktis, jika hanya karena pengembang memiliki lebih banyak waktu untuk menyempurnakan dan mengoptimalkan kode mereka tanpa dikerumuni oleh bug.
Jadi saya pikir dari pengertian praktis ini, struktur data yang tidak berubah sebenarnya dapat membantu kinerja dalam banyak kasus, seaneh kedengarannya. Dunia yang ideal mungkin mencari campuran dari keduanya: struktur data yang tidak berubah dan yang bisa berubah, dengan yang bisa berubah biasanya sangat aman untuk digunakan dalam lingkup yang sangat lokal (mis: lokal ke suatu fungsi), sedangkan yang tidak berubah dapat menghindari sisi eksternal efek langsung dan mengubah semua perubahan pada struktur data menjadi operasi atom menghasilkan versi baru tanpa risiko kondisi ras.