Direktif OpenMP

Region paralel, perulangan worksharing, dan reduksi dengan direktif OpenMP pada gfortran.

Daftar isi

OpenMP adalah model pemrograman untuk paralelisme memori bersama yang diungkapkan melalui direktif (directive), yaitu anotasi berbentuk komentar yang diolah kompiler untuk membentuk region paralel. Topik ini membahas model direktif OpenMP pada Fortran, konstruksi parallel do untuk membagi perulangan ke banyak thread, klausa reduction untuk akumulasi yang aman, serta program penjumlahan satu juta elemen yang dikompilasi dan dijalankan sungguhan dengan gfortran. Setelah topik ini selesai dibaca, pola dasar memparalelkan perulangan dengan OpenMP beserta cara menjaga kebenaran hasilnya sudah terkuasai.

Model direktif OpenMP#

Direktif OpenMP pada Fortran ditulis sebagai komentar yang diawali sentinel !$omp, yaitu penanda khusus yang dikenali kompiler; contoh direktifnya adalah !$omp parallel do. Direktif tersebut berlaku hanya bila program dikompilasi dengan opsi -fopenmp. Kode yang sama tanpa opsi itu tetap valid sebagai program serial karena kompiler memperlakukan sentinelnya sebagai komentar biasa. Sifat ini menjadikan OpenMP bersahabat dengan kode lama, sebab sumbernya tidak perlu ditulis ulang agar tetap bisa dibangun tanpa dukungan OpenMP.

Model eksekusinya berbentuk fork-join, yakni program memulai dengan satu thread, kemudian bercabang menjadi beberapa thread pada region paralel, dan bergabung kembali menjadi satu thread setelah region itu berakhir. Semua thread pada region paralel, yaitu bagian kode yang dibatasi direktif pembuka dan penutupnya, berbagi memori yang sama, sehingga variabel global program dapat dibaca oleh setiap thread. Thread adalah unit eksekusi yang dijadwalkan pada inti prosesor, dan jumlahnya dapat diatur baik melalui variabel lingkungan maupun lewat fungsi pustaka OpenMP yang diakses melalui modul omp_lib.

Region paralel dan pembagian perulangan#

Konstruksi yang paling sering dipakai untuk perulangan adalah !$omp parallel do, yang menggabungkan dua hal sekaligus, yaitu membentuk region paralel dan membagi iterasi perulangan di antara thread yang tersedia. Bentuk pembagian ini disebut worksharing, yakni pembagian kerja yang disepakati seluruh thread anggota region paralel, dan skema defaultnya membagi rentang iterasi menjadi potongan yang besar dan berurutan untuk setiap thread. Variabel kendali perulangan otomatis menjadi privat, yaitu dimiliki satu thread saja, sedangkan variabel lainnya secara default bersifat bersama, yaitu terlihat oleh semua thread.

fortran Contoh: bentuk eksplisit region paralel dan worksharing
! tidak-dijalankan
!$omp parallel private(i)
!$omp do
do i = 1, n
  hasil(i) = v(i) * 2.0
end do
!$omp end do
!$omp end parallel

Bentuk eksplisit di atas memisahkan region paralel dari perulangan worksharing di dalamnya, sedangkan bentuk gabungan parallel do lebih ringkas untuk pola yang paling umum. Keduanya menghasilkan pembagian kerja yang sama, yaitu setiap iterasi dieksekusi tepat satu kali oleh satu thread. Urutan pengerjaan antar-thread tidak dijamin, sehingga hasil yang benar tidak boleh bergantung pada urutan tersebut.

Reduksi untuk akumulasi yang aman#

Klausa reduction menyelesaikan masalah klasik pada program paralel, yaitu mengakumulasi nilai ke satu variabel bersama. Dengan penulisan reduction(+:jumlah), setiap thread menerima salinan privat dari jumlah yang diperbarui secara mandiri, dan setelah region berakhir semua salinan digabungkan dengan operasi penjumlahan. Hasil akhirnya sama persis dengan akumulasi serial, tanpa ada dua thread yang menimpa nilai satu sama lain.

Tanpa klausa ini, penugasan jumlah = jumlah + 1 oleh banyak thread saling bertabrakan, karena setiap thread membaca, menambah, lalu menulis ulang variabel yang sama tanpa satu langkah pun yang berlangsung utuh. Akibatnya sebagian penambahan hilang tertimpa, nilai akhir menjadi lebih kecil dari yang seharusnya, dan angkanya berubah-ubah antar eksekusi. Kerusakan semacam ini disebut berlomba (race), yaitu keadaan ketika hasil program bergantung pada thread mana yang lebih dulu menyelesaikan langkahnya. Peragaan lengkapnya ada pada topik Jebakan paralel di modul ini, sehingga bagian ini cukup menegaskan solusinya, yakni menyatakan operasi penggabungan secara eksplisit melalui reduction.

Menjumlahkan satu juta elemen#

Program berikut menjumlahkan vektor berisi satu juta bilangan bulat dengan parallel do beserta reduction. Variabel lingkungan OMP_NUM_THREADS diatur lebih dahulu agar jumlah thread yang dipakai pasti dan keluarannya dapat direproduksi. Program dikompilasi dan dijalankan dari direktori topik, dan blok berjudul Keluaran di bawahnya adalah hasil asli eksekusi tersebut.

fortran jumlah_paralel.f90
program jumlah_paralel
  use omp_lib
  implicit none
  integer, parameter :: n = 1000000
  integer :: v(n)
  integer(kind=8) :: jumlah
  integer :: i

  do i = 1, n
    v(i) = i
  end do

  print '(A, I0)', 'thread maksimum (omp_get_max_threads) : ', omp_get_max_threads()

  jumlah = 0
  !$omp parallel do reduction(+:jumlah)
  do i = 1, n
    jumlah = jumlah + int(v(i), kind=8)
  end do
  !$omp end parallel do

  print '(A, I0)', 'hasil reduksi jumlah                  : ', jumlah
  print '(A, I0)', 'rumus Gauss n*(n+1)/2                 : ', int(n, kind=8)*int(n+1, kind=8)/2
end program jumlah_paralel
bash
# mengatur jumlah thread lebih dahulu, lalu mengompilasi dan menjalankan
export OMP_NUM_THREADS=2
gfortran -fopenmp jumlah_paralel.f90 -o jumlah_paralel
./jumlah_paralel
text Keluaran
thread maksimum (omp_get_max_threads) : 2
hasil reduksi jumlah                  : 500000500000
rumus Gauss n*(n+1)/2                 : 500000500000

Keluaran di atas menunjukkan dua thread yang tersedia dan hasil penjumlahan sebesar 500000500000, persis sama dengan rumus Gauss yang dihitung secara serial sebagai pembanding. Pola pengisian v(i) = i memungkinkan pemeriksaan seperti itu, karena jumlah satu sampai satu juta memang dapat dihitung dengan rumus tertutup tanpa menjalankan program. Hasil yang cocok dengan perhitungan serial menjadi bukti bahwa reduksi berjalan dengan benar, bukan sekadar terlihat masuk akal.

Mengatur jumlah thread#

Jumlah thread pada saat program dimulai ditentukan oleh variabel lingkungan OMP_NUM_THREADS, dan di dalam kode nilainya dapat dibaca dengan omp_get_max_threads() dari modul omp_lib. Fungsi omp_set_num_threads() juga tersedia untuk mengaturnya dari dalam program, sedangkan klausul num_threads() pada direktif mengatur jumlah thread untuk satu region tertentu. Ketiganya mencapai tujuan yang sama, yaitu menetapkan jumlah thread, sehingga cukup memilih salah satu sesuai kebutuhan; di antara ketiganya, variabel lingkungan paling sering dipakai karena tidak mengharuskan kode diubah.

Ringkasan#

  • Direktif OpenMP pada Fortran ditulis sebagai komentar !$omp dan hanya aktif bila dikompilasi dengan -fopenmp, sedangkan tanpa opsi itu kode tetap merupakan program serial yang valid.
  • Konstruksi !$omp parallel do membentuk region paralel dan membagi iterasi ke banyak thread, dengan variabel kendali yang otomatis menjadi privat.
  • Klausa reduction(+:jumlah) memberi setiap thread salinan akumulasi tersendiri lalu menggabungkannya di akhir region, sehingga hasilnya identik dengan eksekusi serial.
  • Jumlah thread diatur melalui OMP_NUM_THREADS atau fungsi omp_lib, dan kenaikan jumlah thread selalu diukur ulang dampak kinerjanya.

Referensi#