Bagikan melalui


Masalah Umum Migrasi VISUAL C++ ARM

Saat menggunakan pengkompilasi Microsoft C++ (MSVC), kode sumber C++ yang sama mungkin menghasilkan hasil yang berbeda pada arsitektur ARM daripada pada arsitektur x86 atau x64.

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 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 implementasi adalah perilaku yang standar C++ memerlukan vendor pengkompilasi untuk menentukan dan mendokumen. Program dapat dengan aman mengandalkan perilaku yang ditentukan implementasi, meskipun melakukannya mungkin tidak portabel. 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++ sengaja tidak deterministik. Meskipun perilaku dianggap tidak deterministik, 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 tidak ditentukan adalah urutan di mana sub-ekspresi, yang menyertakan argumen ke 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 volatilevariabel -qualified beberapa properti tambahan yang telah digunakan untuk memfasilitasi jenis komunikasi antar-utas tertentu di masa lalu. Tetapi model memori lemah arsitektur ARM tidak mendukung penggunaan ini, juga tidak memerlukan standar C++.

Penting

Meskipun volatile mendapatkan beberapa properti yang dapat digunakan untuk menerapkan bentuk komunikasi antar-utas terbatas pada x86 dan x64, properti tambahan 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 portabel, dan dalam kasus perilaku yang tidak ditentukan atau tidak ditentukan, 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 nilai floating-point ke bilangan bulat 32-bit menjenuhkan ke nilai terdekat yang dapat diwakili bilangan bulat jika nilai floating-point berada di luar rentang yang dapat diwakili bilangan bulat. Pada arsitektur x86 dan x64, konversi membungkus jika bilangan bulat tidak ditandatangani, atau diatur ke -2147483648 jika bilangan bulat ditandatangani. 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 jenis yang tidak ditandatangani dengan benar menjenuhkan jenis yang tidak ditandatangani yang lebih kecil ketika menjenuhkan bilangan bulat 32-bit, tetapi menghasilkan hasil terpotong untuk nilai yang lebih besar dari jenis yang lebih kecil dapat mewakili tetapi terlalu kecil untuk menjenuhkan bilangan bulat 32-bit penuh. Konversi juga menjenuhkan dengan benar untuk bilangan bulat bertanda tangan 32-bit, tetapi pemotongan bilangan bulat yang jenuh dan ditandatangani menghasilkan -1 untuk nilai yang jenuh secara 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 berulang pada setiap kelipatan 64 pada x64, dan setiap kelipatan 256 pada x86, di mana implementasi perangkat lunak 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 4294967296, dan nilainya tidak "membungkus" sampai digeser posisi 64 pada posisi x64, atau 256 pada ARM dan 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 tumpukan tunduk pada perataan. 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 dimaksudkan sebagai padding pada ARM jika tata letak yang diharapkan dari daftar argumen variabel tidak cocok dengan tepat, meskipun mungkin berfungsi untuk subset beberapa 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 terkadang 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 kemungkinan dependensi.

perilaku default kata kunci volatil

Pengkompilasi MSVC mendukung dua interpretasi berbeda dari volatile kualifikasi penyimpanan yang dapat Anda tentukan dengan menggunakan sakelar kompilator. Sakelar /volatile:ms memilih semantik volatil diperpanjang Microsoft yang menjamin pemesanan yang kuat, seperti yang telah menjadi kasus tradisional untuk x86 dan x64 karena model memori yang kuat pada arsitektur tersebut. Sakelar /volatile:iso memilih semantik volatil standar C++ yang ketat yang tidak menjamin pemesanan yang kuat.

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 masih nyaman atau bahkan diharuskan untuk mengkompilasi program ARM untuk 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 sakelar /volatile:ms ; namun, untuk membuat ulang semantik volatil tradisional pada target ARM, pengkompilasi harus menyisipkan penghalang memori di sekitar setiap baca atau tulis volatile variabel untuk memberlakukan pengurutan yang kuat, yang dapat berdampak negatif pada performa.

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 sakelar /volatile:iso untuk membantu menghindari ketergahan yang tidak perlu pada semantik volatil tradisional, dan untuk mempromosikan portabilitas.

Lihat juga

Mengonfigurasi Visual C++ untuk prosesor ARM