Compiler seharusnya menghasilkan assembler (dan akhirnya kode mesin) untuk beberapa mesin, dan umumnya C ++ mencoba bersimpati pada mesin itu.
Bersimpati pada mesin yang mendasarinya secara kasar: membuatnya mudah untuk menulis kode C ++ yang akan memetakan secara efisien ke operasi yang dapat dijalankan mesin dengan cepat. Jadi, kami ingin memberikan akses ke tipe data dan operasi yang cepat dan "alami" pada platform perangkat keras kami.
Konkretnya, pertimbangkan arsitektur mesin tertentu. Mari kita ambil keluarga Intel x86 saat ini.
Manual Pengembang Perangkat Lunak Arsitektur Intel® 64 dan IA-32 vol 1 ( tautan ), bagian 3.4.1 mengatakan:
32-bit register tujuan umum EAX, EBX, ECX, EDX, ESI, EDI, EBP, dan ESP disediakan untuk memegang item berikut:
• Operand untuk operasi logis dan aritmatika
• Operan untuk perhitungan alamat
• Memory pointer
Jadi, kami ingin kompiler menggunakan register EAX, EBX, dll ini saat kompilasi aritmatika C ++ integer sederhana. Ini berarti bahwa ketika saya mendeklarasikan suatu int
, itu harus sesuatu yang kompatibel dengan register ini, sehingga saya dapat menggunakannya secara efisien.
Register selalu berukuran sama (di sini, 32 bit), jadi int
variabel saya akan selalu 32 bit juga. Saya akan menggunakan tata letak yang sama (little-endian) sehingga saya tidak perlu melakukan konversi setiap kali saya memuat nilai variabel ke dalam register, atau menyimpan register kembali ke dalam variabel.
Menggunakan godbolt kita bisa melihat persis apa yang dilakukan kompiler untuk beberapa kode sepele:
int square(int num) {
return num * num;
}
kompilasi (dengan GCC 8.1 dan -fomit-frame-pointer -O3
untuk kesederhanaan) ke:
square(int):
imul edi, edi
mov eax, edi
ret
ini berarti:
- yang
int num
parameter disahkan dalam register EDI, berarti itu persis ukuran dan tata letak Intel berharap untuk mendaftar asli. Fungsi tidak harus mengubah apa pun
- perkalian adalah instruksi tunggal (
imul
), yang sangat cepat
- mengembalikan hasilnya hanyalah masalah menyalinnya ke register lain (penelepon mengharapkan hasilnya dimasukkan ke dalam EAX)
Sunting: kita dapat menambahkan perbandingan yang relevan untuk menunjukkan perbedaan menggunakan merek layout non-asli. Kasus paling sederhana adalah menyimpan nilai dalam sesuatu selain lebar asli.
Menggunakan godbolt lagi, kita dapat membandingkan perkalian asli yang sederhana
unsigned mult (unsigned x, unsigned y)
{
return x*y;
}
mult(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret
dengan kode yang setara untuk lebar non-standar
struct pair {
unsigned x : 31;
unsigned y : 31;
};
unsigned mult (pair p)
{
return p.x*p.y;
}
mult(pair):
mov eax, edi
shr rdi, 32
and eax, 2147483647
and edi, 2147483647
imul eax, edi
ret
Semua instruksi tambahan berkaitan dengan mengubah format input (dua bilangan bulat 31-bit yang tidak ditandatangani) ke dalam format yang dapat ditangani prosesor secara asli. Jika kami ingin menyimpan hasilnya kembali ke nilai 31-bit, akan ada satu atau dua instruksi lain untuk melakukan ini.
Kompleksitas ekstra ini berarti Anda hanya akan repot dengan ini ketika penghematan ruang sangat penting. Dalam hal ini kami hanya menyimpan dua bit dibandingkan dengan menggunakan asli unsigned
atau uint32_t
tipe, yang akan menghasilkan kode yang lebih sederhana.
Catatan tentang ukuran dinamis:
Contoh di atas masih merupakan nilai lebar tetap daripada lebar variabel, tetapi lebar (dan pelurusan) tidak lagi cocok dengan register asli.
Platform x86 memiliki beberapa ukuran asli, termasuk 8-bit dan 16-bit sebagai tambahan untuk 32-bit utama (Saya menggunakan mode 64-bit dan berbagai hal lain untuk kesederhanaan).
Jenis-jenis ini (char, int8_t, uint8_t, int16_t dll) juga secara langsung didukung oleh arsitektur - sebagian untuk kompatibilitas mundur dengan 8086/286/386 / etc yang lebih tua. set instruksi dll.
Sudah pasti bahwa memilih jenis ukuran tetap alami terkecil yang akan cukup, bisa menjadi praktik yang baik - mereka masih cepat, instruksi tunggal memuat dan menyimpan, Anda masih mendapatkan aritmatika asli berkecepatan penuh, dan Anda bahkan dapat meningkatkan kinerja dengan mengurangi kesalahan cache.
Ini sangat berbeda dengan pengodean panjang variabel - Saya telah bekerja dengan beberapa di antaranya, dan mereka mengerikan. Setiap beban menjadi satu lingkaran, bukan satu instruksi. Setiap toko juga merupakan lingkaran. Setiap struktur panjang variabel, jadi Anda tidak bisa menggunakan array secara alami.
Catatan lebih lanjut tentang efisiensi
Dalam komentar selanjutnya, Anda telah menggunakan kata "efisien", sejauh yang saya tahu tentang ukuran penyimpanan. Kami terkadang memilih untuk meminimalkan ukuran penyimpanan - ini bisa menjadi penting ketika kami menyimpan nilai yang sangat besar ke file, atau mengirimkannya melalui jaringan. Imbalannya adalah kita perlu memuat nilai-nilai itu ke dalam register untuk melakukan apa saja dengan mereka, dan melakukan konversi tidak gratis.
Ketika kita membahas efisiensi, kita perlu tahu apa yang kita optimalkan, dan apa timbal baliknya. Menggunakan tipe penyimpanan non-asli adalah salah satu cara untuk memperdagangkan kecepatan pemrosesan untuk ruang, dan terkadang masuk akal. Dengan menggunakan penyimpanan panjang variabel (setidaknya untuk tipe aritmatika), memperdagangkan kecepatan pemrosesan lebih banyak (dan kompleksitas kode dan waktu pengembang) untuk menghemat ruang lebih lanjut yang sering minimal.
Penalti kecepatan yang Anda bayar untuk ini berarti itu hanya bermanfaat ketika Anda harus benar-benar meminimalkan bandwidth atau penyimpanan jangka panjang, dan untuk kasus-kasus itu biasanya lebih mudah untuk menggunakan format yang sederhana dan alami - dan kemudian cukup kompres dengan sistem tujuan umum (seperti zip, gzip, bzip2, xy atau apa pun).
tl; dr
Setiap platform memiliki satu arsitektur, tetapi Anda dapat menghasilkan sejumlah cara berbeda untuk merepresentasikan data. Tidaklah masuk akal jika bahasa apa pun menyediakan jumlah tipe data bawaan yang tidak terbatas. Jadi, C ++ menyediakan akses implisit platform asli, kumpulan tipe data, dan memungkinkan Anda untuk mengkodekan representasi lain (non-pribumi) sendiri.
unsinged
, yang dapat diwakili dengan 1 byte adalah255
. 2) Pertimbangkan overhead menghitung ukuran penyimpanan optimal, dan menyusut / memperluas area penyimpanan, dari suatu variabel, karena nilainya berubah.