Pengoptimuman memori adalah penting untuk menulis sistem perisian berprestasi. Apabila perisian mempunyai jumlah memori yang terhad untuk digunakan, banyak isu boleh timbul apabila memori itu tidak digunakan dengan cekap. Itulah sebabnya pengoptimuman memori adalah penting untuk prestasi keseluruhan yang lebih baik.
Go mewarisi banyak ciri berfaedah C, tetapi apa yang saya perhatikan ialah sebahagian besar orang yang menggunakannya tidak mengetahui kuasa penuh bahasa ini. Salah satu sebabnya mungkin kekurangan pengetahuan tentang cara ia berfungsi pada tahap rendah, atau kekurangan pengalaman dengan bahasa seperti C atau C . Saya menyebut C dan C kerana asas Go cukup banyak dibina berdasarkan ciri-ciri indah C/C . Bukan kebetulan saya akan memetik temu bual Ken Thompson di Google I/O 2012:
Bagi saya, sebab saya bersemangat tentang Go adalah kerana hampir pada masa yang sama kami memulakan Go, saya membaca (atau cuba membaca) piawaian yang dicadangkan C 0x, dan itu adalah meyakinkan untuk saya.
Hari ini, kami akan bercakap tentang cara kami boleh mengoptimumkan program Go kami, dan lebih khusus lagi, cara yang baik untuk menggunakan struct dalam Go. Mari kita nyatakan dahulu apa itu struktur:
Struktur ialah jenis data yang ditentukan pengguna yang mengumpulkan pembolehubah berkaitan jenis berbeza di bawah satu nama.
Untuk memahami sepenuhnya di mana masalahnya, kami akan menyebut bahawa pemproses moden tidak membaca 1 bait pada satu masa dari memori. Bagaimana CPU mengambil data atau arahan yang disimpan dalam memori?
Dalam seni bina komputer, perkataan ialah unit data yang boleh dikendalikan oleh pemproses dalam satu operasi - secara amnya unit memori terkecil yang boleh dialamatkan. Ia adalah kumpulan bit bersaiz tetap (digit binari). Saiz perkataan pemproses menentukan keupayaannya untuk mengendalikan data dengan cekap. Saiz perkataan biasa termasuk 8, 16, 32, dan 64 bit. Sesetengah seni bina pemproses komputer menyokong separuh perkataan, iaitu separuh daripada bilangan bit dalam perkataan, dan kata ganda, iaitu dua perkataan bersebelahan.
Pada masa kini seni bina yang paling biasa ialah 32 bit dan 64 bit. Jika anda mempunyai pemproses 32 bit maka ia bermakna ia boleh mengakses 4 bait pada satu masa yang bermaksud saiz perkataan ialah 4 bait. Jika anda mempunyai pemproses 64 bit, ia boleh mengakses 8 bait pada masa yang bermaksud saiz perkataan ialah 8 bait.
Apabila kami menyimpan data dalam ingatan, setiap perkataan data 32-bit mempunyai alamat yang unik, seperti yang ditunjukkan di bawah.
Rajah. 1 ‑ Memori Boleh Alamat Perkataan
Kita boleh membaca data dalam ingatan dan memuatkannya ke satu daftar menggunakan arahan muatkan perkataan (lw).
Setelah mengetahui teori di atas mari kita lihat apakah amalannya. Untuk huraian kes dengan struktur data struktur, saya akan menunjukkan dengan bahasa C. Struct dalam C ialah jenis data komposit yang membolehkan anda mengumpulkan berbilang pembolehubah bersama-sama dan menyimpannya dalam blok memori yang sama. Seperti yang kami katakan awal CPU mengakses data bergantung kepada seni bina yang diberikan. Setiap jenis data dalam C akan mempunyai keperluan penjajaran.
Jadi mari kita mempunyai yang berikut sebagai struktur mudah:
// structure 1 typedef struct example_1 { char c; short int s; } struct1_t; // structure 2 typedef struct example_2 { double d; int s; char c; } struct2_t;
Dan sekarang cuba kira saiz struktur berikut:
Saiz Struktur 1 = Saiz (char short int) = 1 2 = 3.
Saiz Struktur 2 = Saiz (double int char) = 8 4 1= 13.
Saiz sebenar menggunakan program C mungkin mengejutkan anda.
#include <stdio.h> // structure 1 typedef struct example_1 { char c; short int s; } struct1_t; // structure 2 typedef struct example_2 { double d; int s; char c; } struct2_t; int main() { printf("sizeof(struct1_t) = %lu\n", sizeof(struct1_t)); printf("sizeof(struct2_t) = %lu\n", sizeof(struct2_t)); return 0; }
Output
sizeof(struct1_t) = 4 sizeof(struct2_t) = 16
Seperti yang kita dapat lihat, saiz struktur berbeza daripada yang kami kira.
C dan Go menggunakan teknik yang dikenali sebagai "padding struct" untuk memastikan data diselaraskan dengan betul dalam ingatan, yang boleh menjejaskan prestasi dengan ketara disebabkan oleh kekangan perkakasan dan seni bina. Pelapik dan penjajaran data mematuhi keperluan seni bina sistem, terutamanya untuk mengoptimumkan masa capaian CPU dengan memastikan sempadan data sejajar dengan saiz perkataan.
Mari kita lihat contoh untuk menggambarkan cara Go mengendalikan pelapik dan penjajaran, pertimbangkan struct berikut:
type Employee struct { IsAdmin bool Id int64 Age int32 Salary float32 }
Bool ialah 1 bait, int64 ialah 8 bait, int32 ialah 4 bait dan float32 ialah 4 bait = 17 bait(jumlah).
Mari sahkan saiz struct dengan memeriksa program Go yang disusun:
package main import ( "fmt" "unsafe" ) type Employee struct { IsAdmin bool Id int64 Age int32 Salary float32 } func main() { var emp Employee fmt.Printf("Size of Employee: %d\n", unsafe.Sizeof(emp)) }
Output
Size of Employee: 24
Saiz yang dilaporkan ialah 24 bait, bukan 17. Percanggahan ini disebabkan oleh penjajaran memori. Untuk memahami cara penjajaran berfungsi, kita perlu memeriksa struktur dan memvisualisasikan memori yang didudukinya.
Rajah 2 - Reka Letak Memori Tidak Dioptimumkan
Struktur Pekerja akan menggunakan 8*3 = 24 bait. Anda lihat masalahnya sekarang, terdapat banyak lubang kosong dalam susun atur Pekerja (jurang yang dicipta oleh peraturan penjajaran dipanggil "padding").
Memahami cara penjajaran memori dan pelapik boleh menjejaskan prestasi aplikasi adalah penting. Khususnya, penjajaran data memberi kesan kepada bilangan kitaran CPU yang diperlukan untuk mengakses medan dalam struktur. Pengaruh ini kebanyakannya timbul daripada kesan cache CPU, dan bukannya kitaran jam mentah itu sendiri, kerana tingkah laku cache sangat bergantung pada lokaliti data dan penjajaran dalam blok memori.
CPU moden mengambil data daripada memori ke dalam perantara yang lebih pantas dipanggil cache, disusun dalam blok bersaiz tetap (biasanya 64 bait). Apabila data diselaraskan dengan baik dan disetempatkan dalam talian cache yang sama atau lebih sedikit, CPU boleh mengaksesnya dengan lebih cepat disebabkan oleh pengurangan operasi pemuatan cache.
Pertimbangkan struktur Go berikut untuk menggambarkan penjajaran lemah berbanding optimum:
// structure 1 typedef struct example_1 { char c; short int s; } struct1_t; // structure 2 typedef struct example_2 { double d; int s; char c; } struct2_t;
CPU membaca data dalam saiz perkataan dan bukannya saiz bait. Seperti yang saya jelaskan pada permulaan perkataan dalam sistem 64-bit ialah 8 bait, manakala perkataan dalam sistem 32-bit ialah 4 bait. Ringkasnya, CPU membaca alamat dalam berbilang saiz perkataannya. Untuk mengambil passportId berubah, CPU kami mengambil dua kitaran untuk mengakses data dan bukannya satu kitaran. Kitaran pertama akan mengambil memori 0 hingga 7 dan kitaran seterusnya akan mengambil yang lain. Dan ini adalah tidak cekap- kami memerlukan penjajaran struktur data. Dengan hanya menjajarkan data, komputer memastikan bahawa var passportId boleh diambil dalam kitaran SATU CPU.
Rajah 3 - Membandingkan Kecekapan Capaian Memori
Padding ialah kunci untuk mencapai penjajaran data. Padding berlaku kerana CPU moden dioptimumkan untuk membaca data daripada memori pada alamat sejajar. Penjajaran ini membolehkan CPU membaca data dalam satu operasi.
Rajah 4 - Hanya menjajarkan data
Tanpa padding, data mungkin tidak sejajar, membawa kepada berbilang akses memori dan prestasi yang lebih perlahan. Oleh itu, walaupun padding mungkin membazirkan sedikit memori, ia memastikan program anda berjalan dengan cekap.
Struktur sejajar menggunakan memori yang lebih sedikit kerana ia mempunyai susunan medan struct yang lebih baik berbanding dengan Tidak Sejajar. Disebabkan pelapik, dua struktur data 13 bait menjadi 16 bait dan 24 bait masing-masing. Oleh itu, anda menjimatkan memori tambahan dengan hanya menyusun semula medan struct anda.
Rajah 5 - Mengoptimumkan Susunan Medan
Data yang tidak sejajar boleh memperlahankan prestasi kerana CPU mungkin memerlukan berbilang kitaran untuk mengakses medan tidak sejajar. Sebaliknya, data yang dijajarkan dengan betul meminimumkan beban talian cache, yang penting untuk prestasi, terutamanya dalam sistem yang kelajuan memori menjadi halangan.
Mari kita lakukan penanda aras mudah untuk membuktikannya:
#include <stdio.h> // structure 1 typedef struct example_1 { char c; short int s; } struct1_t; // structure 2 typedef struct example_2 { double d; int s; char c; } struct2_t; int main() { printf("sizeof(struct1_t) = %lu\n", sizeof(struct1_t)); printf("sizeof(struct2_t) = %lu\n", sizeof(struct2_t)); return 0; }
Output
sizeof(struct1_t) = 4 sizeof(struct2_t) = 16
Seperti yang anda lihat, melintasi Aligned sememangnya mengambil masa yang lebih singkat berbanding rakan sejawatannya.
Padding ditambahkan untuk memastikan setiap medan struct berbaris dengan betul dalam ingatan berdasarkan keperluannya, seperti yang kita lihat sebelum ini. Tetapi sementara ia membolehkan akses yang cekap, padding juga boleh membuang ruang jika medan tidak disusun dengan baik.
Memahami cara menjajarkan medan struct dengan betul untuk meminimumkan sisa memori akibat pelapik adalah penting untuk penggunaan memori yang cekap, terutamanya dalam aplikasi kritikal prestasi. Di bawah, saya akan memberikan contoh dengan struktur yang tidak sejajar dan kemudian menunjukkan versi yang dioptimumkan bagi struktur yang sama.
Dalam struct yang tidak dijajarkan dengan baik, medan dipesan tanpa mengambil kira saiz dan keperluan penjajarannya, yang boleh membawa kepada padding tambahan dan peningkatan penggunaan memori:
// structure 1 typedef struct example_1 { char c; short int s; } struct1_t; // structure 2 typedef struct example_2 { double d; int s; char c; } struct2_t;
Jumlah ingatan boleh jadi 1 (bool) 7 (padding) 8 (float64) 4 (int32) 4 (padding) 16 (rentetan) = 40 bait.
Struktur yang dioptimumkan menyusun medan daripada saiz terbesar hingga terkecil, mengurangkan atau menghapuskan keperluan untuk pelapik tambahan dengan ketara:
#include <stdio.h> // structure 1 typedef struct example_1 { char c; short int s; } struct1_t; // structure 2 typedef struct example_2 { double d; int s; char c; } struct2_t; int main() { printf("sizeof(struct1_t) = %lu\n", sizeof(struct1_t)); printf("sizeof(struct2_t) = %lu\n", sizeof(struct2_t)); return 0; }
Jumlah memori kemudiannya akan mengandungi 8 (float64) 16 (rentetan) 4 (int32) 1 (bool) 3 (padding) = 32 bait.
Jom buktikan perkara di atas:
sizeof(struct1_t) = 4 sizeof(struct2_t) = 16
Output
type Employee struct { IsAdmin bool Id int64 Age int32 Salary float32 }
Mengurangkan saiz struktur daripada 40 bait kepada 32 bait bermakna pengurangan 20% dalam penggunaan memori setiap contoh Orang. Ini boleh membawa kepada penjimatan yang besar dalam aplikasi di mana banyak kejadian sedemikian dibuat atau disimpan, meningkatkan kecekapan cache dan berpotensi mengurangkan bilangan kehilangan cache.
Penjajaran data ialah faktor penting dalam mengoptimumkan penggunaan memori dan meningkatkan prestasi sistem. Dengan menyusun data struktur dengan betul, penggunaan memori bukan sahaja menjadi lebih cekap tetapi juga lebih pantas dari segi masa baca CPU, menyumbang dengan ketara kepada kecekapan sistem keseluruhan.
Atas ialah kandungan terperinci Mengoptimumkan Penggunaan Memori dalam Go: Menguasai Penjajaran Struktur Data. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!