Kode paralel memperkenalkan kelas kesalahan yang tidak pernah muncul pada kode serial, dan sebagian besar di antaranya tidak terdeteksi kompiler. Topik ini membahas tiga jebakan yang paling sering ditemui, yaitu race condition pada data bersama, nilai not-a-number yang muncul dari operasi numerik tak sah, serta false sharing yang menggerus kinerja tanpa membuat hasil salah. Setiap jebakan diperagakan atau dijelaskan melalui program yang dikompilasi dan dijalankan sungguhan dengan gfortran, lengkap dengan cara menghindarinya. Setelah topik ini selesai dibaca, penyebab ketiganya dan solusi yang menyertainya sudah dikenali.
Race condition pada penghitung bersama#
Race condition (race condition) adalah keadaan ketika hasil program bergantung pada urutan eksekusi yang tidak dapat diprediksi, dan penyebabnya adalah dua thread atau lebih mengakses lokasi memori yang sama tanpa pengaturan. Penugasan jumlah = jumlah + 1 terlihat seperti satu langkah, tetapi prosesornya menjalankan tiga langkah, yaitu membaca nilai, menambahkannya, lalu menulis kembali. Apabila dua thread membaca nilai yang sama sebelum salah satunya selesai menulis, pembaruan milik thread yang lebih lambat tertimpa dan satu penambahan hilang begitu saja.
Program berikut menjalankan penghitung tanpa pengaturan tersebut sepuluh kali dengan dua thread. Program sengaja tidak mencetak nilai penghitungnya satu per satu, karena nilai persisnya berubah-ubah antar eksekusi, dan keluarannya hanya melaporkan kesimpulan logika atas kesepuluh percobaan. Karena kesimpulan itu sendiri ikut berubah antar eksekusi, blok perintah di bawah ini tidak disertai blok Keluaran yang menuntut kesamaan persis; sebagai gantinya, satu eksekusi nyata ditampilkan terpisah dalam blok berjudul Satu eksekusi nyata yang teramati.
program penghitung_balapan
use omp_lib
implicit none
integer, parameter :: n = 200000
integer(kind=8) :: jumlah
integer :: i, percobaan
logical :: ada_keliru
ada_keliru = .false.
do percobaan = 1, 10
jumlah = 0
! penghitung diperbarui tanpa reduction dan tanpa critical: operasi
! baca-tambah-tulis oleh banyak thread dapat saling menimpa
!$omp parallel do
do i = 1, n
jumlah = jumlah + 1
end do
!$omp end parallel do
if (jumlah /= int(n, kind=8)) then
ada_keliru = .true.
end if
end do
print '(A, I0)', 'thread yang digunakan : ', omp_get_max_threads()
print '(A, I0)', 'percobaan yang dilakukan : ', 10
print '(A, L1)', 'minimal satu percobaan keliru : ', ada_keliru
print '(A, I0)', 'nilai yang benar setiap percobaan : ', int(n, kind=8)
end program penghitung_balapan# mengatur jumlah thread lebih dahulu, lalu mengompilasi dan menjalankan
export OMP_NUM_THREADS=2
gfortran -fopenmp penghitung_balapan.f90 -o penghitung_balapan
./penghitung_balapanthread yang digunakan : 2
percobaan yang dilakukan : 10
minimal satu percobaan keliru : T
nilai yang benar setiap percobaan : 200000Eksekusi yang teramati di atas melaporkan T, yaitu minimal satu dari sepuluh percobaan menghasilkan nilai yang keliru. Pada eksekusi ulang yang lain, baris kesimpulan yang sama pernah mencetak F, yaitu kesepuluh percobaan menghasilkan nilai yang benar. Hasil F seperti itu muncul apabila kedua thread kebetulan tidak pernah berjalan bersamaan pada satu percobaan pun, sehingga tidak ada penambahan yang saling menimpa. Nilai T atau F itu baru menunjukkan gejala tingkat akhir, yaitu benar atau tidaknya hasil keseluruhan; gejala yang lebih rinci terlihat pada nilai penghitung yang diamati langsung selama perulangan berlangsung.
Pengamatan langsung terhadap nilai penghitung di akhir setiap percobaan memperlihatkan dua pola hasil. Pola pertama adalah nilai akhir yang tepat 200000, artinya tidak ada penambahan yang saling menimpa karena kedua thread kebetulan bergantian tanpa bertabrakan. Pola kedua adalah nilai akhir yang jauh lebih kecil, berkisar sekitar 96000 sampai 130000, yang berarti sebagian besar penambahan hilang karena ditimpa oleh thread yang lain. Ketidakpastian antara dua pola inilah inti dari race condition, yaitu program yang sama dapat memberikan keluaran berbeda setiap kali dijalankan, dan kesalahannya berbahaya karena program tetap berhenti normal tanpa pesan apa pun.
Solusi dengan reduction dan critical#
Solusi pertama dan yang paling disarankan untuk akumulasi adalah klausa reduction, yang mekanismenya telah dibahas pada topik Direktif OpenMP di modul ini. Setiap thread memperbarui salinan privatnya sendiri, lalu hasil semua salinan digabungkan sekali di akhir region. Program berikut adalah versi aman dari penghitung yang sama, dan blok berjudul Keluaran di bawahnya adalah hasil asli eksekusinya.
program penghitung_aman
use omp_lib
implicit none
integer, parameter :: n = 200000
integer(kind=8) :: jumlah
integer :: i
jumlah = 0
!$omp parallel do reduction(+:jumlah)
do i = 1, n
jumlah = jumlah + 1
end do
!$omp end parallel do
print '(A, I0)', 'thread yang digunakan : ', omp_get_max_threads()
print '(A, I0)', 'jumlah akhir program : ', jumlah
print '(A, I0)', 'nilai yang benar : ', int(n, kind=8)
end program penghitung_aman# mengatur jumlah thread lebih dahulu, lalu mengompilasi dan menjalankan
export OMP_NUM_THREADS=2
gfortran -fopenmp penghitung_aman.f90 -o penghitung_aman
./penghitung_amanthread yang digunakan : 2
jumlah akhir program : 200000
nilai yang benar : 200000Dengan reduction, hasil akhirnya selalu 200000 dan sama dengan nilai yang benar, berapa pun jumlah thread yang dipakai. Solusi kedua adalah pernyataan critical yang membuat satu blok kode hanya boleh dieksekusi satu thread pada satu waktu, sebagaimana diperlihatkan fragmen berikut.
! tidak-dijalankan
!$omp critical
jumlah = jumlah + 1
!$omp end criticalBentuk critical menghasilkan nilai yang benar, tetapi seluruh thread mengantre untuk masuk ke blok yang sama, sehingga paralelismenya praktis hilang pada perulangan yang badannya pendek. Untuk pola akumulasi, reduction mempertahankan kerja paralel penuh dan hanya menggabungkan hasilnya sekali, sehingga keduanya tidak dapat ditukar begitu saja.
NaN dan operasi numerik tak sah#
Nilai not-a-number (NaN) adalah representasi khusus tipe real yang menandai hasil perhitungan yang tidak terdefinisi, misalnya pembagian nol dengan nol atau akar kuadrat dari bilangan negatif. Program berikut membangkitkan kedua operasi tersebut dan mendeteksinya dengan fungsi ieee_is_nan dari modul ieee_arithmetic. Program dikompilasi dan dijalankan dari direktori topik, dan blok berjudul Keluaran di bawahnya adalah hasil asli eksekusi tersebut.
program demo_nan
use, intrinsic :: ieee_arithmetic
implicit none
real :: nol, negatif, hasil_bagi, hasil_akar
nol = 0.0
negatif = -1.0
hasil_bagi = nol / nol
hasil_akar = sqrt(negatif)
print '(A, L1)', 'ieee_is_nan(0.0/0.0) : ', ieee_is_nan(hasil_bagi)
print '(A, L1)', 'ieee_is_nan(sqrt(-1.0)) : ', ieee_is_nan(hasil_akar)
print '(A, F6.1)', 'cetak 0.0/0.0 : ', hasil_bagi
print '(A, ES12.3)', 'cetak sqrt(-1.0) : ', hasil_akar
end program demo_nan# mengompilasi dan menjalankan program dari direktori topik
gfortran demo_nan.f90 -o demo_nan
./demo_nanieee_is_nan(0.0/0.0) : T
ieee_is_nan(sqrt(-1.0)) : T
cetak 0.0/0.0 : NaN
cetak sqrt(-1.0) : NaNProgram ini deterministik, yaitu kedua operasi selalu menghasilkan NaN pada setiap eksekusi, berbeda dengan race condition yang keluarannya berpindah-pindah. Pada program paralel, NaN sering muncul dari pola yang berkaitan dengan race condition, misalnya variabel real yang dibaca sebelum diinisialisasi sehingga isinya berupa pola bit yang tidak mewakili bilangan yang sah. Nilai NaN juga menular, karena setiap perhitungan yang melibatkan NaN menghasilkan NaN lagi, sehingga satu elemen yang rusak dapat merusak seluruh hasil akhir. Pemeriksaan ieee_is_nan pada hasil antara yang mencurigakan adalah cara termurah untuk menemukan sumbernya.
False sharing#
False sharing adalah penurunan kinerja yang terjadi ketika dua thread menulis ke variabel berbeda yang kebetulan bertetangga pada satu baris cache (cache line) yang sama. Prosesor memindahkan memori antar inti dalam satuan baris cache, bukan satuan variabel, sehingga dua tulisan yang secara logika tidak bersaing tetap saling membatalkan salinan cache milik inti lain. Gejalanya khas, yaitu waktu eksekusi tidak turun meskipun jumlah thread dinaikkan, sementara hasil perhitungannya selalu benar sehingga kesalahan ini tidak pernah tampil sebagai nilai yang salah.
! tidak-dijalankan
! dua thread menulis elemen yang berselang-seling, sehingga satu baris
! cache terus berpindah antar inti
do i = 1, n
indeks = mod(i - 1, 2) + 1
hasil(indeks) = hasil(indeks) + berat(i)
end doCara menghindarinya adalah menyusun akses data agar setiap thread bekerja pada wilayah memori yang terpisah dan berurutan, misalnya membagi rentang perulangan menjadi potongan besar dan berurutan lewat klausul schedule(static), yaitu klausul yang menetapkan pembagian iterasi menjadi potongan berurutan yang tetap untuk setiap thread. Pola pembagian kerja semacam ini senada dengan skema default worksharing yang telah dibahas pada topik Direktif OpenMP. Pola lainnya adalah memberi setiap thread array privatnya sendiri, lalu menggabungkan seluruh array itu di akhir region, sehingga penulisan antar thread tidak pernah bertetangga. Biaya penggabungan yang ditambahkan biasanya jauh lebih kecil daripada biaya baris cache yang terus berpindah.
Ringkasan#
- Race condition muncul ketika banyak thread memperbarui satu lokasi data tanpa pengaturan, akibatnya nilai akhir berubah-ubah antar eksekusi, dan solusinya adalah
reductionuntuk akumulasi ataucriticaluntuk blok yang harus berjalan tunggal. - NaN menandai hasil perhitungan yang tidak terdefinisi, menular ke seluruh perhitungan selanjutnya, dan dideteksi dengan
ieee_is_nandari modulieee_arithmetic. - False sharing terjadi karena dua thread menulis ke satu baris cache yang sama, tidak mengubah hasil tetapi menggerus kinerja, dan dihindari dengan memisahkan wilayah tulis antar thread.
Referensi#
- Learn: tutorial resmi Fortran — fortran-lang — Kumpulan tutorial resmi komunitas Fortran yang menjadi rujukan fitur bahasa pada jalur ini.
- Situs resmi OpenMP — OpenMP Architecture Review Board — Sumber resmi spesifikasi OpenMP yang menjadi dasar klausul reduction, critical, dan schedule pada topik ini.
- The GNU Fortran Compiler — GNU Project
— Manual gfortran yang merinci dukungan modul
ieee_arithmeticpada kompiler yang dipakai di sini.