Fungsi inline

Apa yang dilakukan dan tidak dilakukan oleh keyword inline.

Daftar isi

Kata kunci inline pada sebuah fungsi mengajukan dua permintaan sekaligus. Pertama, pemanggilan diajukan untuk digantikan langsung oleh badan fungsinya. Kedua, definisi fungsi tersebut dibolehkan muncul berulang di banyak unit penerjemahan. Pemahaman yang keliru umumnya terjadi pada permintaan pertama, padahal kekuatan sebenarnya berada pada permintaan kedua. Topik ini membedah apa yang benar-benar dilakukan inline, apa yang tidak dijamin olehnya, dan perannya pada berkas header.

Apa yang dilakukan inline#

Pemanggilan fungsi biasa menimbulkan biaya kecil berupa penyimpanan konteks, penyalinan argumen, dan lompatan kontrol. Sebuah fungsi yang ditandai inline mengajukan permintaan agar badannya disisipkan langsung di titik pemanggilan, sehingga lompatan itu hilang. Kebutuhan semacam itu sering muncul pada fungsi kecil yang dipanggil berulang, misalnya pengecekan batas atau perkalian sederhana.

cpp kuadrat-inline.cpp
#include <iostream>

inline int kuadrat(int n) {
    return n * n;
}

int main() {
    for (int i = 1; i <= 5; ++i) {
        if (i > 1) {
            std::cout << ' ';
        }
        std::cout << kuadrat(i);
    }
    std::cout << '\n';
    return 0;
}
text Keluaran
1 4 9 16 25

Program ini menghasilkan keluaran yang sama persis dengan versi tanpa inline, karena kata kunci tersebut tidak mengubah makna program, hanya petunjuk cara menerjemahkannya. Sifat yang tidak berubah ini penting untuk pengujian, karena versi inline dan versi biasa dapat ditukar tanpa mengubah keluaran apa pun. Perhatikan pula bahwa fungsi yang ditandai inline lazim dituliskan langsung di berkas header, kebiasaan yang alasannya dijelaskan pada bagian berikutnya.

Definisi fungsi anggota yang dituliskan langsung di dalam badan kelas dianggap inline secara otomatis, sehingga kata kuncinya tidak perlu ditulis pada kasus itu. Kebiasaan yang sama berlaku pada fungsi kecil pustaka standar yang didefinisikan di header, misalnya operasi pembacaan pada std::vector. Compiler memperlakukan keduanya dengan kaidah yang sama seperti fungsi bebas yang ditandai eksplisit.

Permintaan, bukan jaminan#

Standar C++ menyebut inline sebagai permintaan, dan compiler bebas mengabaikannya. Fungsi yang besar, mengandung perulangan, atau memanggil dirinya sendiri biasanya tidak disisipkan, sedangkan fungsi kecil yang tidak ditandai inline sering disisipkan begitu saja ketika optimasi diaktifkan. Keputusan akhir selalu diambil compiler berdasarkan analisis biayanya.

Dokumentasi GCC menyatakan bahwa GCC menyisipkan fungsi-fungsi kecil secara otomatis pada tingkat optimasi tertentu, dan dokumentasi itu juga memperingatkan bahwa ukuran program bisa membengkak apabila penyisipan dilakukan berlebihan. Dengan kata lain, inline yang diletakkan pada setiap fungsi bukanlah strategi optimasi, melainkan pemborosan yang menambah waktu kompilasi.

Compiler tertentu menyediakan petunjuk yang lebih keras, misalnya atribut always_inline pada GCC dan __forceinline pada compiler Microsoft. Petunjuk semacam itu spesifik vendor dan tetap tidak dapat dipenuhi pada semua bentuk fungsi, misalnya fungsi rekursif yang penyisipannya tidak akan pernah selesai. Kode yang bergantung pada petunjuk vendor menjadi tidak mudah dipindahkan, sehingga pemakaiannya disisakan untuk titik yang terukur benar-benar kritis.

Cara kerja yang benar adalah menandai fungsi kecil yang dipanggil berulang pada perulangan yang paling sering dijalankan, lalu membiarkan compiler memutuskan sisanya. Untuk memastikan penyisipan benar-benar terjadi, hasilnya diperiksa melalui keluaran perakitan (assembly) atau alat profiling, bukan diasumsikan dari kata kuncinya.

Peran inline pada header#

Aturan One Definition Rule (ODR) melarang sebuah fungsi non-inline didefinisikan lebih dari satu kali di seluruh program. Berkas header yang dipakai oleh banyak berkas .cpp menimbulkan persis situasi itu, karena setiap unit penerjemahan menerima salinan isi header. Tanpa perlindungan tambahan, definisi lengkap di header menghasilkan error linker berupa multiple definition.

cpp Definisi tanpa inline di header ditolak linker
// tidak-dijalankan
// Disisipkan oleh dua berkas .cpp — linker melaporkan multiple definition
double rataRata(double a, double b) {
    return (a + b) / 2.0;
}

Kata kunci inline memberikan pengecualian yang dibutuhkan. Fungsi inline boleh didefinisikan berulang di banyak unit penerjemahan, dengan syarat seluruh definisinya identik, dan linker kemudian menggabungkan salinan-salinan itu menjadi satu. Program dengan dua unit penerjemahan berikut memanfaatkan kebebasan tersebut.

cpp statistik.h
// tidak-dijalankan (butuh build multi-berkas; perintah ada di bawah)
#ifndef STATISTIK_H
#define STATISTIK_H

inline double rataRata(double a, double b) {
    return (a + b) / 2.0;
}

#endif
cpp main.cpp
// tidak-dijalankan (butuh build multi-berkas; perintah ada di bawah)
#include <iostream>
#include "statistik.h"

void cetakLaporan();

int main() {
    std::cout << "Rata-rata dua nilai: " << rataRata(78.0, 92.0) << '\n';
    cetakLaporan();
    return 0;
}
cpp laporan.cpp
// tidak-dijalankan (butuh build multi-berkas; perintah ada di bawah)
#include <iostream>
#include "statistik.h"

void cetakLaporan() {
    std::cout << "Rata-rata laporan: " << rataRata(64.0, 76.0) << '\n';
}
bash
# kompilasi program dari dua unit penerjemahan
g++ main.cpp laporan.cpp -o aplikasi-laporan
# jalankan program hasil kompilasi
./aplikasi-laporan
text Keluaran
Rata-rata dua nilai: 85
Rata-rata laporan: 70

Berkas statistik.h disisipkan oleh main.cpp dan laporan.cpp, sehingga definisi rataRata muncul di dua unit penerjemahan. Karena fungsinya ditandai inline, linker menerima dan menggabungkannya, sedangkan tanpa tanda itu program yang sama gagal dibangun. Inilah alasan fungsi kecil pada header pustaka, termasuk fungsi anggota yang didefinisikan di dalam kelas, memakai inline atau dianggap inline secara otomatis.

Sejak C++17, kata kunci inline juga berlaku pada variabel, sehingga satu variabel dapat didefinisikan langsung di header dan disertakan oleh banyak unit penerjemahan. Fasilitas ini menghapus kebutuhan memisahkan deklarasi dan definisi variabel static milik kelas. Syaratnya sama seperti fungsi, yaitu seluruh definisi variabel inline harus identik di seluruh proyek.

Kesalahan umum#

Menganggap inline sebagai jaminan penyisipan adalah kesalahan pertama. Program yang dirancang mengandalkan penyisipan untuk kecepatannya perlu diukur, karena compiler dapat mengabaikan permintaan tersebut dan bahkan menyisipkan fungsi tanpa diminta. Keputusan optimasi diserahkan pada compiler, sementara inline dipakai sesuai makna aturan definisinya. Aturan itu sendiri ditetapkan standar C++, sehingga penerapannya tidak bergantung pada tingkat optimasi yang dipilih.

Kesalahan kedua adalah memakai inline untuk menjadikan fungsi konstanta saat kompilasi. Penyisipan terjadi setelah kompilasi dan tidak menciptakan nilai konstanta, sehingga kebutuhan semacam itu dijawab constexpr, bukan inline. Kesalahan ketiga adalah menandai semua fungsi sebagai inline demi kecepatan, padahal efeknya hanya memperbesar program dan memperpanjang kompilasi.

Perbandingan kecepatan yang tidak adil juga menyesatkan. Pengukuran yang sah membandingkan dua build dengan perilaku identik, tingkat optimasi yang sama, dan pengulangan beberapa kali agar hasilnya stabil. Pengukuran juga sebaiknya disertai profil pemakaian yang nyata, karena penyisipan yang menguntungkan satu jalur bisa membebani jalur lain.

Ringkasan#

  • inline mengajukan permintaan penyisipan badan fungsi di titik pemanggilan, dan compiler bebas menerima atau menolaknya.
  • Kontribusi utama inline adalah pada aturan definisi, karena fungsi inline boleh didefinisikan identik di banyak unit penerjemahan sehingga definisi di header menjadi sah.
  • Optimasi kecepatan tetap ditentukan compiler, sedangkan kebutuhan nilai saat kompilasi ditangani constexpr.

Referensi#