Memory Fence Membatasi Urutan Memori Antar-Core

Prosesor dapat menjalankan operasi memori dengan kebebasan yang lebih besar daripada yang tampak dari urutan kode sumber. Load dapat dimulai lebih awal, store dapat menunggu di buffer, lalu lintas koherensi cache dapat selesai pada waktu berbeda, dan operasi yang saling independen dapat berlangsung tumpang tindih. Teknik-teknik ini meningkatkan throughput, tetapi perangkat lunak konkuren membutuhkan aturan yang presisi untuk memublikasikan dan mengamati state bersama.

Memory fence menempatkan batas pengurutan di sekitar operasi memori tertentu. Instruksi ini biasanya tidak mengosongkan seluruh cache, tidak menserialkan seluruh prosesor, dan tidak membuat semua core menjalankan satu alur instruksi. Perannya lebih sempit: membatasi hasil pengurutan memori apa saja yang boleh terjadi melintasi sebuah batas yang ditentukan.

Perbedaan ini penting untuk kebenaran sekaligus performa. Pengurutan yang terlalu lemah dapat menampilkan state yang tidak diizinkan oleh protokol sinkronisasi. Pengurutan yang terlalu kuat dapat menghambat overlap yang sebenarnya berguna dan meningkatkan latensi.

Urutan program dan urutan memori adalah konsep yang berbeda

Bayangkan satu thread menyiapkan data lalu memublikasikan sebuah flag:

data = 42
ready = 1

Thread lain memeriksa flag tersebut sebelum membaca data:

if ready == 1:
    use data

Protokol yang diinginkan sederhana: ketika flag publikasi terlihat, pembaruan data yang terjadi sebelumnya juga seharusnya tersedia sesuai memory model bahasa dan mesin.

Urutan di kode sumber saja bukan jaminan sinkronisasi yang portabel. Compiler dapat mengubah urutan operasi jika aturan bahasa mengizinkannya, dan prosesor dapat menggunakan store buffer, speculative execution, serta mekanisme lain yang memengaruhi urutan yang terlihat oleh agen lain.

Karena itu, kode konkuren yang benar bergantung pada semantik sinkronisasi dari bahasa pemrograman, sistem operasi, atau arsitektur tingkat rendah. Pada level mesin, fence merupakan salah satu alat untuk menerapkan semantik tersebut.

Fence membatasi hasil, bukan menyalin data

Kesalahpahaman umum adalah menganggap fence sebagai instruksi yang mendorong byte yang telah diubah langsung dari satu core ke cache core lain. Koherensi cache dan memory ordering menyelesaikan masalah yang berhubungan, tetapi bukan masalah yang sama.

Koherensi secara umum menjaga konsistensi cache yang berpartisipasi untuk satu lokasi memori tertentu. Aturan ordering menentukan hubungan di antara akses ke beberapa lokasi.

Fence berkontribusi pada ordering. Bergantung pada arsitektur dan jenis fence, instruksi ini dapat mewajibkan load atau store sebelumnya mencapai titik pengurutan tertentu sebelum operasi sesudahnya boleh tampak dalam urutan yang bertentangan.

Jaminan pastinya bergantung pada arsitektur. Full fence dapat membatasi lebih banyak kombinasi daripada load-only atau store-only fence. Beberapa instruction set menyediakan beberapa varian fence agar perangkat lunak dapat meminta hanya pengurutan yang benar-benar diperlukan.

Store buffer membuat kebutuhan ordering terlihat dalam praktik

Store buffer memungkinkan core terus bekerja sebelum setiap store selesai melalui hierarki cache. Tanpa mekanisme ini, core akan lebih sering berhenti sambil menunggu kepemilikan dan transaksi koherensi.

Misalnya sebuah core melakukan:

store A
store B

Arsitektur menentukan observasi apa yang boleh dilakukan core lain dan dalam urutan apa. Store-ordering fence yang tepat di antara operasi tersebut dapat melarang reordering tertentu ketika memory model dasar belum memberikan batas yang dibutuhkan.

Ini tidak berarti store pertama harus sampai ke DRAM sebelum store kedua ada. Sistem koheren modern dapat memenuhi aturan ordering sementara kedua nilai tetap berada sepenuhnya di struktur cache. Properti yang penting adalah observabilitas secara arsitektural, bukan perjalanan fisik ke memori utama.

Semantik acquire dan release sering menghindari full fence

Banyak operasi sinkronisasi menggunakan semantik acquire dan release alih-alih barrier yang paling restriktif.

Operasi release mencegah operasi memori relevan sebelumnya bergerak melewati titik publikasi menurut memory model yang berlaku. Operasi acquire mencegah operasi relevan sesudahnya bergerak ke sebelum titik observasi.

Pola publikasi dapat digambarkan seperti ini:

producer:
    write payload
    release-store ready = 1

consumer:
    acquire-load ready
    read payload

Ketika acquire mengamati release yang sesuai berdasarkan aturan sinkronisasi, state producer yang lebih awal dapat terurut sebelum akses consumer yang datang kemudian.

Ordering yang terarah seperti ini sering lebih murah daripada memasang full barrier di sekitar setiap operasi shared memory. Biaya nyata bergantung pada arsitektur prosesor, keadaan cache, kontensi, pemetaan compiler, dan primitive sinkronisasi yang dipakai.

Compiler barrier dan CPU fence bekerja pada lapisan berbeda

Compiler barrier membatasi transformasi kode oleh compiler. CPU fence membatasi memory ordering pada level mesin sesuai definisi instruction-set architecture.

Keduanya tidak otomatis saling menggantikan.

Inline assembly yang mengeluarkan hardware fence tetapi tidak memberikan constraint memori yang memadai kepada compiler masih dapat mengizinkan perpindahan instruksi yang bermasalah di sekitar fence tersebut. Sebaliknya, compiler-only barrier dapat mempertahankan urutan instruksi hasil kompilasi tanpa memberikan ordering tambahan pada level mesin di prosesor yang memang mengizinkan reordering tersebut.

Library atomic tingkat tinggi ada sebagian untuk menghubungkan kedua lapisan ini dengan benar. Operasinya memberi constraint semantik kepada compiler dan membiarkan implementasi memilih instruksi yang sesuai untuk arsitektur target.

Hardware dengan ordering lebih kuat tidak menghapus aturan bahasa

Keluarga prosesor memiliki jaminan default memory ordering yang berbeda. Sebagian menawarkan model yang relatif kuat untuk load dan store biasa, sementara yang lain mengizinkan lebih banyak reordering yang dapat terlihat dan lebih sering mengandalkan barrier eksplisit atau instruksi atomic khusus.

Kode konkuren portabel tidak boleh menyimpulkan aturannya hanya dari perilaku satu prosesor. Memory model bahasa dapat menganggap data race tanpa sinkronisasi sebagai tidak valid atau berada di luar jaminan program, meskipun pengujian berulang pada satu mesin tampak stabil.

Karena itu, operasi atomic pada level source merupakan kontrak utama untuk kode portabel. Implementasi compiler memetakan kontrak tersebut ke mekanisme ordering yang tersedia di prosesor target.

Kernel tingkat rendah, runtime, device driver, dan library sinkronisasi bekerja lebih dekat ke arsitektur dan mungkin menggunakan instruksi fence secara langsung.

Fence dapat menjadi bottleneck throughput

Ordering memiliki biaya karena dapat mengurangi overlap di antara operasi memori. Fence yang restriktif dapat memaksa pekerjaan yang masih tertunda mencapai kondisi arsitektural tertentu sebelum pekerjaan berikutnya berjalan. Dalam kondisi shared state berat atau kontensi cache line tinggi, penundaan ini bisa signifikan.

Biayanya bukan angka siklus yang tetap. Ia bergantung pada store yang masih outstanding, kepemilikan koherensi, keadaan hierarki memori, kelas fence, mikroarsitektur, dan dependency di sekitar instruksi.

Karena itu, penempatan barrier pertama-tama adalah keputusan kebenaran, baru kemudian target optimasi. Menghapus edge ordering yang dibutuhkan dapat menciptakan bug konkurensi langka yang lolos benchmark. Menambahkan barrier di mana-mana dapat mempertahankan kebenaran tetapi membuang sumber daya eksekusi.

Target yang berguna adalah primitive sinkronisasi paling lemah yang masih memenuhi relasi memory model yang dibutuhkan.

Device memory dapat membutuhkan aturan ordering terpisah

Memory-mapped I/O menambahkan batas lain. Register perangkat dapat memiliki aturan ordering dan efek samping yang berbeda dari memori cacheable biasa.

Sebagai contoh, perangkat lunak dapat menyiapkan descriptor di memori normal lalu menulis register perangkat untuk memberi tahu hardware agar mulai mengonsumsinya. Barrier yang dibutuhkan harus cocok dengan aturan DMA, koherensi cache, dan I/O ordering pada platform tersebut. Asumsi umum mengenai sinkronisasi CPU-ke-CPU bisa tidak cukup.

Sistem operasi biasanya menyediakan primitive barrier khusus untuk skenario seperti ini. Abstraksi tersebut menyimpan kebutuhan spesifik platform dan dapat membedakan shared-memory ordering biasa dari ordering yang menghadap perangkat.

Ini juga menjadi alasan untuk tidak mengganti primitive kernel atau driver yang terdokumentasi dengan instruksi fence ad hoc. Operasi yang benar dapat bergantung pada jenis memori dan observer eksternal yang terlibat.

Ordering adalah kontrak antara compiler, prosesor, dan perangkat lunak

Memory fence bukan tombol global untuk flush cache. Fence adalah mekanisme ordering yang digunakan ketika eksekusi konkuren membutuhkan batas yang lebih kuat daripada aturan default.

Efek praktisnya datang dari kontrak di sekelilingnya. Atomic pada level source membatasi transformasi compiler, aturan instruction set mendefinisikan ordering yang terlihat mesin, koherensi menjaga konsistensi cache line bersama, dan protokol sinkronisasi menghubungkan semuanya menjadi jalur komunikasi yang valid.

Fence yang presisi karena itu bukan soal menghentikan prosesor, melainkan melarang observasi tertentu. Cara pandang yang lebih sempit ini membuat pemilihan fence lebih mudah dianalisis dan menjaga biaya performa tetap sebanding dengan ordering yang benar-benar dibutuhkan program.