Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Dokumen ini menjelaskan beberapa masalah umum yang mungkin Anda temui saat memigrasikan kode dari arsitektur x86 atau x64 ke arsitektur ARM. Ini juga menjelaskan cara menghindari masalah ini, dan cara menggunakan kompilator untuk membantu mengidentifikasinya.
Nota
Ketika artikel ini mengacu pada arsitektur ARM, artikel ini berlaku untuk ARM32 dan ARM64.
Sumber masalah migrasi
Banyak masalah yang mungkin Anda temui saat memigrasikan kode dari arsitektur x86 atau x64 ke arsitektur ARM terkait dengan konstruksi kode sumber yang mungkin memanggil perilaku yang tidak ditentukan, ditentukan implementasi, atau tidak ditentukan.
Perilaku yang tidak ditentukan adalah perilaku yang tidak ditentukan oleh standar C++, dan itu disebabkan oleh operasi yang tidak memiliki hasil yang wajar: misalnya, mengonversi nilai floating-point menjadi bilangan bulat yang tidak ditandatangani, atau menggeser nilai oleh sejumlah posisi yang negatif atau melebihi jumlah bit dalam jenis yang dipromosikan.
Perilaku yang ditentukan oleh implementasi adalah perilaku yang standar C++ mengharuskan vendor kompiler untuk menentukan dan mendokumentasikan. Program dapat dengan aman bergantung pada perilaku yang ditentukan oleh implementasi, meskipun melakukannya mungkin tidak dapat dipindahkan. Contoh perilaku yang ditentukan implementasi termasuk ukuran jenis data bawaan dan persyaratan penyelarasannya. Contoh operasi yang mungkin dipengaruhi oleh perilaku yang ditentukan implementasi adalah mengakses daftar argumen variabel.
Perilaku yang tidak ditentukan adalah perilaku yang dibiarkan standar C++ dengan sengaja tidak deterministik. Meskipun perilaku dianggap nondeterministik, pemanggilan perilaku yang tidak ditentukan ditentukan oleh implementasi kompilator. Namun, tidak ada persyaratan bagi vendor kompilator untuk menentukan hasil atau menjamin perilaku yang konsisten antara pemanggilan yang sebanding, dan tidak ada persyaratan untuk dokumentasi. Contoh perilaku yang belum ditentukan adalah urutan di mana sub-ekspresi, yang termasuk argumen dalam panggilan fungsi, dievaluasi.
Masalah migrasi lainnya dapat dikaitkan dengan perbedaan perangkat keras antara arsitektur ARM dan x86 atau x64 yang berinteraksi dengan standar C++ secara berbeda. Misalnya, model memori yang kuat dari arsitektur x86 dan x64 memberikan variabel yang memenuhi syarat volatile beberapa properti tambahan yang telah digunakan untuk memfasilitasi jenis komunikasi antar-utas tertentu di masa lalu. Namun, model memori lemah arsitektur ARM tidak mendukung penggunaan ini, dan standar C++ juga tidak mensyaratkannya.
Penting
Meskipun volatile mendapatkan beberapa properti yang dapat digunakan untuk menerapkan bentuk komunikasi antar-utas terbatas pada x86 dan x64, properti ini tidak cukup untuk menerapkan komunikasi antar-utas secara umum. Standar C++ merekomendasikan agar komunikasi tersebut diimplementasikan dengan menggunakan primitif sinkronisasi yang sesuai sebagai gantinya.
Karena platform yang berbeda mungkin mengekspresikan jenis perilaku ini secara berbeda, porting perangkat lunak antar platform bisa sulit dan rawan bug jika tergantung pada perilaku platform tertentu. Meskipun banyak dari jenis perilaku ini dapat diamati dan mungkin tampak stabil, mengandalkannya setidaknya tidak dapat diandalkan, serta dalam kasus perilaku yang tidak didefinisikan atau tidak dispesifikasi, juga merupakan kesalahan. Bahkan perilaku yang dikutip dalam dokumen ini tidak boleh diandalkan, dan dapat berubah dalam kompilator atau implementasi CPU di masa mendatang.
Contoh masalah migrasi
Sisa dokumen ini menjelaskan bagaimana perilaku yang berbeda dari elemen bahasa C++ ini dapat menghasilkan hasil yang berbeda pada platform yang berbeda.
Konversi titik mengambang ke bilangan bulat yang tidak ditandatangani
Pada arsitektur ARM, konversi dari nilai floating-point ke bilangan bulat 32-bit mendekati nilai terdekat yang dapat diwakili oleh bilangan bulat, jika nilai floating-point berada di luar rentang representasi bilangan bulat. Pada arsitektur x86 dan x64, konversi akan berputar kembali jika bilangan bulatnya tanpa tanda, atau diatur ke -2147483648 jika bilangan bulatnya bertanda. Tidak satu pun dari arsitektur ini secara langsung mendukung konversi nilai floating-point ke jenis bilangan bulat yang lebih kecil; sebaliknya, konversi dilakukan ke 32 bit, dan hasilnya dipotong ke ukuran yang lebih kecil.
Untuk arsitektur ARM, kombinasi saturasi dan pemotongan berarti konversi ke tipe tak bertanda secara benar akan menjenuhkan tipe tak bertanda yang lebih kecil ketika menjenuhkan bilangan bulat 32-bit, tetapi menghasilkan hasil terpotong untuk nilai yang lebih besar dari yang dapat diwakili oleh tipe yang lebih kecil tetapi terlalu kecil untuk menjenuhkan bilangan bulat 32-bit penuh. Konversi juga menjenuhkan dengan benar untuk bilangan bulat bertanda 32-bit, tetapi pemotongan bilangan bulat bertanda yang jenuh menghasilkan -1 untuk nilai yang jenuh positif dan 0 untuk nilai yang jenuh negatif. Konversi ke bilangan bulat bertanda tangan yang lebih kecil menghasilkan hasil terpotong yang tidak dapat diprediksi.
Untuk arsitektur x86 dan x64, kombinasi perilaku pembungkusan untuk konversi bilangan bulat yang tidak ditandatangani dan valuasi eksplisit untuk konversi bilangan bulat yang ditandatangani pada luapan, bersama dengan pemotongan, membuat hasil untuk sebagian besar shift tidak dapat diprediksi jika terlalu besar.
Platform ini juga berbeda dalam cara mereka menangani konversi NaN (Not-a-Number) ke jenis bilangan bulat. Di ARM, NaN dikonversi ke 0x00000000; pada x86 dan x64, dikonversi ke 0x80000000.
Konversi floating-point hanya dapat diandalkan jika Anda tahu bahwa nilai berada dalam rentang jenis bilangan bulat yang sedang dikonversi.
Perilaku operator shift (<<>>)
Pada arsitektur ARM, nilai dapat digeser ke kiri atau kanan hingga 255 bit sebelum pola mulai diulang. Pada arsitektur x86 dan x64, pola diulang pada setiap kelipatan 32 kecuali sumber pola adalah variabel 64-bit. Dalam hal ini, pola akan berulang pada setiap kelipatan 64 pada x64 dan setiap kelipatan 256 pada x86, tergantung pada implementasi perangkat lunak yang digunakan. Misalnya, untuk variabel 32-bit yang memiliki nilai 1 digeser ke kiri 32 posisi, pada ARM hasilnya adalah 0, pada x86 hasilnya adalah 1, dan pada x64 hasilnya juga 1. Namun, jika sumber nilai adalah variabel 64-bit, maka hasilnya pada ketiga platform adalah 4294967296, dan nilainya tidak "berputar kembali" sampai digeser sebanyak 64 posisi pada x64, atau 256 posisi pada ARM dan diulang untuk x86.
Karena hasil operasi shift yang melebihi jumlah bit dalam jenis sumber tidak terdefinisi, pengkompilasi tidak diperlukan untuk memiliki perilaku yang konsisten dalam semua situasi. Misalnya, jika kedua operan pergeseran diketahui pada waktu kompilasi, pengkompilasi dapat mengoptimalkan program dengan menggunakan rutinitas internal untuk mengolah hasil pergeseran dan kemudian mengganti hasilnya sebagai pengganti operasi shift. Jika jumlah shift terlalu besar, atau negatif, hasil rutinitas internal mungkin berbeda dari hasil ekspresi shift yang sama seperti yang dijalankan oleh CPU.
Perilaku argumen variabel (varargs)
Pada arsitektur ARM, parameter dari daftar argumen variabel yang diteruskan pada stack harus diselaraskan. Misalnya, parameter 64-bit diselaraskan pada batas 64-bit. Pada x86 dan x64, argumen yang diteruskan pada tumpukan tidak tunduk pada perataan dan pengemasan dengan erat. Perbedaan ini dapat menyebabkan fungsi variadik seperti printf membaca alamat memori yang seharusnya menjadi padding pada ARM jika tata letak daftar argumen variabel tidak persis sesuai, meskipun mungkin berfungsi untuk sebagian subset nilai pada arsitektur x86 atau x64. Pertimbangkan contoh ini:
// notice that a 64-bit integer is passed to the function, but '%d' is used to read it.
// on x86 and x64 this may work for small values because %d will "parse" the low-32 bits of the argument.
// on ARM the calling convention will align the 64-bit value and the code will print a random value
printf("%d\n", 1LL);
Dalam hal ini, bug dapat diperbaiki dengan memastikan bahwa spesifikasi format yang benar digunakan sehingga perataan argumen dipertimbangkan. Kode ini benar:
// CORRECT: use %I64d for 64-bit integers
printf("%I64d\n", 1LL);
Urutan evaluasi argumen
Karena prosesor ARM, x86, dan x64 sangat berbeda, mereka dapat menyajikan persyaratan yang berbeda untuk mengkompilasi implementasi, dan juga peluang yang berbeda untuk pengoptimalan. Karena itu, bersama dengan faktor lain seperti pengaturan konvensi panggilan dan pengoptimalan, pengkompilasi mungkin mengevaluasi argumen fungsi dalam urutan yang berbeda pada arsitektur yang berbeda atau ketika faktor lain diubah. Hal ini dapat menyebabkan perilaku aplikasi yang bergantung pada urutan evaluasi tertentu berubah secara tidak terduga.
Kesalahan semacam ini dapat terjadi ketika argumen ke fungsi memiliki efek samping yang memengaruhi argumen lain ke fungsi dalam panggilan yang sama. Biasanya dependensi semacam ini mudah dihindari tetapi dapat dikaburkan oleh dependensi yang sulit dibedakan atau oleh kelebihan beban operator. Pertimbangkan contoh kode ini:
handle memory_handle;
memory_handle->acquire(*p);
Ini tampak terdefinisi dengan baik, tetapi jika -> dan * merupakan operator yang kelebihan beban, maka kode ini diterjemahkan ke sesuatu yang menyerupai ini:
Handle::acquire(operator->(memory_handle), operator*(p));
Dan jika ada dependensi antara operator->(memory_handle) dan operator*(p), kode mungkin mengandalkan urutan evaluasi tertentu, meskipun kode asli terlihat seperti tidak ada dependensi yang mungkin.
volatile perilaku bawaan kata kunci
Pengkompilasi Microsoft C++ (MSVC) mendukung dua interpretasi berbeda dari volatile kualifikasi penyimpanan yang dapat Anda tentukan dengan menggunakan sakelar kompilator. Sakelar /volatile:ms memilih semantik volatile yang diperpanjang oleh Microsoft yang menjamin urutan yang kuat, seperti yang merupakan kasus tradisional untuk x86 dan x64 karena model memori yang kuat pada arsitektur tersebut. Sakelar /volatile:iso memilih semantik volatile standar C++ yang tidak menjamin pengurutan yang ketat.
Pada arsitektur ARM (kecuali ARM64EC), defaultnya adalah /volatile:iso karena prosesor ARM memiliki model memori yang diurutkan dengan lemah, dan karena perangkat lunak ARM tidak memiliki warisan untuk mengandalkan semantik /volatile:ms yang diperluas dan biasanya tidak harus berinteraksi dengan perangkat lunak yang melakukannya. Namun, terkadang tetap berguna atau bahkan diharuskan untuk mengompilasi program ARM agar menggunakan semantik yang diperluas. Misalnya, mungkin terlalu mahal untuk memindahkan program untuk menggunakan semantik ISO C++, atau perangkat lunak driver mungkin harus mematuhi semantik tradisional agar berfungsi dengan benar. Dalam kasus ini, Anda dapat menggunakan opsi /volatile:ms; namun, untuk membuat ulang semantik volatil tradisional pada target ARM, pengkompilasi harus menyisipkan penghalang memori di sekitar setiap operasi baca atau tulis terhadap variabel volatile untuk menegakkan urutan yang ketat, yang dapat berdampak negatif terhadap kinerja.
Pada arsitektur x86, x64, dan ARM64EC, defaultnya adalah /volatile:ms karena banyak perangkat lunak yang telah dibuat untuk arsitektur ini dengan menggunakan MSVC bergantung padanya. Ketika Anda mengkompilasi program x86, x64, dan ARM64EC, Anda dapat menentukan opsi /volatile:iso untuk membantu menghindari ketergantungan yang tidak perlu pada semantik volatile tradisional dan mempromosikan portabilitas.