SMART POINTER
Pointer adalah jenis khusus variabel yang menyimpan alamat memori dari suatu objek, yang dapat digunakan untuk merujuk pada objek tersebut. Dalam C++, memungkinkan untuk melewatkan objek baik secara nilai atau sebagai referensi, dengan yang terakhir menjadi jauh lebih cepat untuk objek yang lebih besar. Ini tidak hanya lebih cepat tetapi juga lebih konservatif dalam hal penggunaan memori karena tidak ada data yang disalin sementara. Mengembalikan objek yang lebih besar secara nilai dan melewatkan mereka sebagai argumen fungsi dapat jauh lebih mahal secara komputasi, dibandingkan dengan bekerja dengan hanya menggunakan pointer mereka.
Sebuah contoh bagus untuk aplikasi pointer di OpenFOAM adalah fungsi-fungsi yang mengimplementasikan algoritma interpolasi. Interpolasi beroperasi pada lapangan, yang dalam CFD seringkali memiliki ratusan ribu komponen per inti CPU. Jika algoritma interpolasi diimplementasikan sebagai sebuah fungsi, fungsi tersebut dapat memberikan hasil lapangan yang diperlukan dengan dua cara, dengan mengembalikan hasil sebagai sebuah objek
atau dengan memodifikasi argumen hasil yang dilewatkan sebagai referensi tidak konstan ke fungsi
Opsi pertama lebih disukai untuk algoritma diskritisasi (operator) karena sering terdiri dari ekspresi aritmatika untuk membangun model matematika. Sebagai contoh, pertimbangkan kode persamaan kekekalan momentum yang diambil dari solver interFoam:
Hasil dari penjumlahan operator yang bertindak pada lapangan rho, U, dan rhoPhi akan menjadi matriks koefisien (fvVectorMatrix). Sebagai konsekuensinya, titik-titik berikut harus dipenuhi dari kode di atas:
– fvVectorMatrix harus dapat disalin,
– semua operator harus mengembalikan fvVectorMatrix,
– operator penambahan fvVectorMatrix harus mengembalikan fvVectorMatrix.
Jika operator ddt dan div diimplementasikan sebagai fungsi yang mengambil parameter yang dapat dimodifikasi, menulis model matematika dengan mudah (biasanya disebut sebagai equation mimicking) tidak akan mungkin. Matriks yang dikembalikan oleh fungsi cukup besar, jadi mengembalikannya secara nilai memperkenalkan biaya menciptakan objek sementara. OpenFOAM menghindari membuat operasi salinan yang tidak perlu dengan cara yang eksplisit tidak bergantung pada optimasi kompilator, dengan bantuan smart pointer OpenFOAM yang diimplementasikan secara native. Sebuah smart pointer diinisialisasi dalam fungsi interpolasi, dan dikembalikan secara nilai. Hal yang sama berlaku untuk operasi lain yang melibatkan diskritisasi persamaan, seperti skema konveksi. Sebagai contoh, operator divergensi fvmDiv dari skema konveksi Gauss menginisialisasi sebuah smart pointer (tmp<fvMatrix<Type>>).
Setelah itu, dilakukan perhitungan yang bertanggung jawab untuk mendefinisikan elemen-elemen dalam matriks koefisien yang ditunjukkan.
Smart pointer yang diinisialisasi di awal fungsi kemudian dikembalikan secara nilai:
Mengembalikan sebuah smart pointer ke matriks volume yang terbatas (tfvm) secara nilai menghasilkan operasi salinan dari smart pointer. Namun, ada perbedaan yang signifikan antara salinan dari smart pointer dan salinan dari seluruh objek matriks: nilai pointer hanya alamat dari matriks, dan bukan seluruh matriks itu sendiri. Pendekatan ini secara substansial meningkatkan efisiensi implementasi.
INFO
Menghindari operasi salinan yang tidak perlu memiliki nama yang lebih pendek yang umum digunakan: eliminasi salinan. Eliminasi salinan dapat diberlakukan dengan berbagai cara dalam bahasa pemrograman C++: optimasi kompilator (Return Value Optimization, Named RVO), Expression Templates (ET), atau dengan menggunakan referensi rvalue dan semantik perpindahan yang disediakan oleh standar bahasa C++11.
Secara umum, ketika pointer digunakan, mereka menunjuk pada objek yang dibuat di heap menggunakan operator new:
Karena bahasa pemrograman C++ dengan sengaja tidak mendukung pengumpulan sampah otomatis, programmer bertanggung jawab untuk melepaskan sumber daya.
Oleh karena itu, setiap pemanggilan operator new harus diikuti oleh pemanggilan delete yang sesuai. Tentu saja, pemanggilan operator delete harus ditempatkan di lokasi yang sesuai, seperti destruktor kelas. Ini membuka kemungkinan bahwa programmer dengan mudah lupa untuk menghapus pointer, yang menghasilkan kebocoran memori. Sebagai sumber kesalahan alternatif, mengakses bagian dari memori yang dirujuk oleh pointer yang sudah dihapus akan menghasilkan perilaku yang tidak terdefinisi. Kedua masalah ini akan terjadi saat runtime dan biasanya menjadi sumber kesalahan yang sulit ditemukan dan diperbaiki. Untuk menghindari kedua masalah tersebut, penanganan langsung dari pointer mentah harus dihindari. Dalam C++, penanganan pointer mentah telah digantikan oleh idiom yang disebut Resource Acquisition Is Initialization (RAII).
Idiom RAII menyatakan bahwa pointer mentah harus diinkapsulasi ke dalam sebuah kelas, yang destruktornya mengurus penghapusan pointer dan melepaskan sumber daya pada tempat yang sesuai dalam kode. Mengadaptasi pointer mentah dalam kelas-kelas seperti itu, dan menyediakan fungsionalitas yang berbeda untuk pointer mentah yang diadaptasi telah mengarah pada pengembangan smart pointer yang disebut. Berbagai smart pointer ada dan menyediakan fungsionalitas yang berbeda. OpenFOAM mengimplementasikan dua dari smart pointer tersebut: autoPtr dan tmp.
Dengan asumsi bahwa variabel lingkungan diatur dengan mendaur ulang skrip konfigurasi etc/bashrc dari repositori kode contoh, kode aplikasi contoh dibuat tersedia di folder $PRIMER_EXAMPLES_SRC/applications/test/. Pembaca yang tertarik untuk mengikuti tutorial yang disajikan dalam bagian-bagian berikut langkah demi langkah, perlu membuat aplikasi eksekutif baru – testSmartPointers. Membuat aplikasi baru di OpenFOAM disederhanakan, karena skrip tersedia yang menghasilkan direktori-direktori kerangka untuk aplikasi. Untuk membuat aplikasi baru, sebuah direktori dipilih di mana kode aplikasi akan ditempatkan, dan perintah-perintah berikut dijalankan:
Garis terakhir menggantikan direktori penempatan untuk file biner aplikasi dari direktori platform, ke direktori biner aplikasi pengguna.
INFO
Adalah praktik yang baik untuk membangun aplikasi Anda sendiri ke dalam $FOAM_USER_APPBIN, yang perlu ditentukan dalam file konfigurasi pembangunan Make/options.
INFO
Pada bagian ini, compiler gcc digunakan. Perlu diingat hal ini saat membaca tentang bendera compiler spesifik dalam teks.
Menggunakan smart pointer autoPtr
Untuk mengilustrasikan bagaimana autoPtr digunakan, kelas baru perlu didefinisikan untuk contoh-contoh yang digunakan. Kelas ini harus memberi tahu pengguna setiap kali konstruktor dan destruktor nya dipanggil. Untuk keperluan contoh ini, kelas ini mewarisi dari kelas templat bidang dasar Field<Type>, dan dinamai infoField. Kelas lainnya bisa digunakan, karena optimisasi yang dilakukan oleh compiler untuk operasi salinan tidak tergantung pada ukuran objek.
Untuk memulai, template kelas infoField dapat didefinisikan seperti yang ditunjukkan di bawah ini:
Kelas template mewarisi dari Field<Type> dan menggunakan hal berikut:
– konstruktor kosong
– konstruktor salinan
– destruktor
– operator penugasan
Setiap kali fungsi-fungsi tersebut digunakan, pernyataan Info memberi sinyal panggilan khusus ke aliran keluaran standar. Ini hanya dilakukan untuk tujuan memperoleh informasi tentang fungsi mana yang dieksekusi.
Untuk melanjutkan contoh, sebuah template fungsi perlu didefinisikan yang mengembalikan objek secara nilai
Nama tipe yang digunakan dalam contoh ini dipersingkat untuk mengurangi jumlah pengetikan yang tidak perlu.
Di fungsi utama implementasikan baris-baris berikut:
Mengompilasi dan menjalankan aplikasi dengan opsi Debug atau Opt akan menghasilkan hasil yang sama persis, meskipun opsi Debug mematikan optimasi kompiler. Seperti yang disebutkan di awal bagian: compiler sangat cerdas dalam mengenali fakta bahwa objek sementara hanya dikembalikan untuk dibuang setelah penugasan. Keuntungan lain dari tidak menggunakan lapangan geometris adalah bahwa aplikasi contoh kecil ini tidak perlu dieksekusi dalam direktori kasus simulasi OpenFOAM. Ini bisa dipanggil langsung dari direktori di mana kode disimpan. Menjalankan aplikasi menghasilkan keluaran berikut:
Memperhatikan setiap baris keluaran secara individu, sambil membandingkannya dengan kode fungsi valueReturn, secara menarik mengungkapkan bahwa variabel pengembalian sementara tidak pernah dibangun. Konstruksi salinan hilang untuk pernyataan pengembalian fungsi, karena fungsi mengembalikan secara nilai, serta panggilan destruktor yang sesuai. Alasan untuk jenis perilaku ini adalah optimasi eliminasi salinan yang dilakukan secara otomatis oleh compiler, yang ada bahkan dalam mode Debug. Untuk menghapus optimasi ini, bendera compiler tambahan dapat ditambahkan ke Make/options:
Bendera compiler gcc -fno-elide-constructors akan mencegah compiler melakukan optimasi, penting untuk mengaktifkan pelacakan apa yang terjadi di valueReturn, yang sebaliknya akan dioptimalkan oleh compiler.
INFO
Ingat untuk memanggil wclean sebelum menjalankan wmake. Modifikasi pada file Make/options tidak dikenali oleh sistem pembangunan wmake sebagai modifikasi kode sumber yang memerlukan re-kompilasi.
Mengompilasi dan menjalankan aplikasi dengan file opsi sebagaimana ditentukan di atas menghasilkan keluaran berikut:
Keluaran menunjukkan penciptaan dan penghapusan objek sementara yang tidak perlu. Mengelakkan salinan objek sementara dengan melakukan konstruksi di tempat di mana fungsi mengembalikan telah menjadi pilihan standar untuk compiler. Sering kali fitur ini tidak dapat dinonaktifkan bahkan ketika menghilangkan optimasi dalam mode Debug dengan bendera compiler:
Untuk menonaktifkan optimasi eliminasi salinan konstruktor, bendera compiler -fno-elide-constructors harus dilewati secara eksplisit dalam Make/options.
TIP
Contoh di atas memiliki satu tujuan utama: Untuk menunjukkan bahwa tidak perlu menggunakan smart pointer autoPtr dalam ekspresi di mana objek sementara dengan nama dikembalikan secara anyway. Pada compiler modern, bahkan saat mengompilasi dalam mode debug (untuk OpenFOAM ini berarti mengatur $WM_COMPILE_OPTION menjadi Debug), optimasi ini diaktifkan secara default.
Beberapa contoh paling menonjol untuk penggunaan autoPtr berada dalam model yang menggunakan RTS, seperti turbulenceModel dan fvOptions. Kelas dasar tertentu diinstansiasi berdasarkan kata kunci yang ditentukan pengguna dalam kamus. Menggunakan kelas turbulenceModel sebagai contoh, kelas dasar digunakan sebagai argumen templat untuk autoPtr dalam misalnya aplikasi solver dan RTS kemudian dapat menginstansiasi model turbulensi tertentu ke dalam autoPtr. RTS memungkinkan pengguna kelas untuk menginstansiasi objek dari suatu kelas hierarki tertentu pada saat runtime. Menginstansiasi objek dengan cara itu memungkinkan kemampuan C++ untuk mengakses objek kelas turunan melalui pointer atau referensi kelas dasar. Ini biasanya disebut sebagai polimorfisme dinamis.
INFO
Tidak mungkin untuk mencakup semua detail dari standar bahasa C++ yang digunakan oleh OpenFOAM dalam buku ini. Setiap kali konstruk C++ ditemui yang tidak terdengar familiar, harus dilihat di tempat lain.
Model turbulensi biasanya diinstansiasi dalam createFields.H solver tertentu, yang disertakan sebelum awal loop waktu. Baris yang relevan adalah sebagai berikut:
Tentu saja, autoPtr digunakan untuk menyimpan model turbulensi. Jika pointer mentah digunakan untuk mengakses objek RASModel, baris lain dalam kode solver akan diperlukan, yang menghapus pointer mentah, untuk membebaskan memori pada akhir solver. Dengan autoPtr menjadi smart pointer, penghapusan ini dilakukan secara otomatis dalam destruktor autoPtr. Kode untuk metode pointer mentah (yang untungnya tidak digunakan di mana pun di OpenFOAM) akan terlihat seperti ini:
Pada akhir kode solver, baris-baris berikut akan diperlukan:
Tentu saja, untuk satu pointer, menambahkan baris untuk melepaskan sumber daya mungkin tidak menjadi masalah. Namun, RTS digunakan untuk: model transportasi, kondisi batas, fvOptions, skema diskritisasi, skema interpolasi, skema gradien, dan sebagainya. Semua objek itu kecil dibandingkan dengan hal-hal seperti medan dan mesh, jadi apakah mereka bisa dikembalikan secara nilai? Dari sudut pandang efisiensi – ya, terutama mengingat Optimasi Nilai Kembali (RVO) diimplementasikan oleh semua compiler modern. Dari sudut pandang fleksibilitas – tidak ada peluang untuk melakukan itu, karena polimorfisme dinamis mengandalkan akses melalui pointer atau referensi. Menjaga fleksibilitas runtime tinggi, berarti mengandalkan pointer atau referensi
TIPS
Menggunakan autoPtr atau tmp diperlukan saat RTS digunakan untuk memilih objek pada saat runtime.
Contoh berikut meneliti antarmuka autoPtr dan semantik salin berbasis kepemilikan. autoPtr memiliki objek yang ditunjuk. Ini diharapkan, karena RAII memerlukan smart pointer untuk menangani pelepasan sumber daya sehingga programmer tidak perlu melakukannya. Akibatnya, membuat salinan autoPtr rumit – menyalin autoPtr membuat autoPtr asli tidak valid dan mentransfer kepemilikan objek ke salinan. Untuk melihat bagaimana ini bekerja, pertimbangkan fungsi utama dari potongan kode berikut:
Penting untuk dicatat bahwa begitu salinan objek autoPtr dilakukan, kepemilikan objek yang ditunjuk ditransfer. Dengan mengomentari baris yang menyebabkan kesalahan segmen, keluaran aplikasi yang dihasilkan terlihat seperti ini:
Jelas hanya satu konstruktor dan destruktor kelas infoScalarField yang dipanggil, meskipun objek autoPtr dilewatkan secara nilai. Oleh karena itu autoPtr dapat digunakan untuk menyimpan operasi salin yang tidak perlu.
Menggunakan smart pointer tmp
Smart pointer tmp mencegah salinan yang tidak perlu dari objek dengan melakukan perhitungan referensi. Perhitungan referensi adalah proses di mana objek yang sama dipindahkan. Dalam konteks ini, perhitungan referensi dilakukan menggunakan kelas refCount. Struktur data ini adalah kelas dasar yang penting untuk setiap kelas yang seharusnya disimpan dalam smart pointer tmp. Ini dibungkus oleh smart pointer, dengan jumlah referensi ke objek ini meningkat setiap kali salinan atau penugasan dari smart pointer dilakukan. Patuh dengan RAII, destruktor dari pointer tmp bertanggung jawab untuk menghancurkan objek yang dibungkus. Destruktor memeriksa jumlah refCount yang dimiliki objek dalam cakupan saat ini. Jika jumlah ini lebih besar dari nol, destruktor hanya mengurangi refCount sebesar satu dan memungkinkan objek untuk tetap ada. Segera setelah destruktor dipanggil dalam situasi di mana refCount ke objek yang dibungkus telah mencapai nol, destruktor menghapus objek.
TIPS
Definisi smart pointer tmp dapat ditemukan di $FOAM_SRC/OpenFOAM/memory/tmp
Ada yang menarik saat menggunakan smart pointer tmp: kelas template pointer tidak dibuat bertanggung jawab untuk menghitung referensi. Perhitungan referensi diharapkan dari tipe yang dibungkus. Ini dapat dengan mudah diperiksa saat menguji destruktor kelas:
Atribut ptr_ adalah pointer mentah yang dibungkus ke objek tipe T dan destruktor mencoba mengakses dua fungsi anggota:
Sebagai hasilnya, objek yang dibungkus harus mematuhi antarmuka kelas tertentu. Cara lain untuk menguji penangkapan ini adalah dengan mencoba menggunakan tmp dengan kelas yang trivial yang dapat didefinisikan sebagai berikut:
Kemudian mencoba membungkus kelas ini ke dalam smart pointer tmp:
Kode di atas menghasilkan kesalahan berikut:
Dengan kesalahan ini, compiler mengeluh tentang fakta bahwa fungsi anggota yang disebutkan sebelumnya tidak diimplementasikan oleh testClass. Karena OpenFOAM membuat objek melakukan perhitungan referensi, itu telah dienkapsulasi ke dalam kelas yang diwarisi oleh objek tersebut, yang dinamai refCount. Kelas refCount mengimplementasikan penghitung referensi dan fungsi anggota terkait.
TIPS
Untuk menggunakan tmp<class T> di OpenFOAM, tipe T dari objek yang dibungkus harus mewarisi dari refCount.
Jika testClass dimodifikasi untuk mewarisi refCount:
Maka dapat dibungkus dengan tmp. Untuk melihat bagaimana penghitungan referensi bekerja dan bagaimana konstruksi objek yang tidak perlu dihindari, tmp dapat digunakan dengan kelas infoScalarField, yang digunakan dalam contoh yang menjelaskan autoPtr. Kelas refCount memungkinkan pengguna untuk mendapatkan informasi tentang jumlah referensi saat ini dengan fungsi anggota refCount::count(), yang digunakan dalam contoh berikut. Ruang lingkup buatan digunakan untuk mengurangi masa pakai objek tmp sehingga destruktor mereka dipanggil. Ini akan terjadi dalam kode program reguler ketika panggilan fungsi bertingkat atau loop bersarang hadir. Berikut adalah contoh kodenya:
Ini menghasilkan keluaran baris perintah berikut:
Sebuah konstruktor tunggal dan output destruktor yang sesuai dari kelas infoField menunjukkan bahwa hanya satu konstruksi dan penghancuran yang dilakukan, meskipun objek tmp dilewatkan secara nilai sebagai argumen fungsi.