1 poin oleh GN⁺ 5 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • Saat mengecilkan struct pencatatan ICMP Echo Request pada sistem pemantauan konektivitas, penggunaan memori ring buffer turun dari 12KiB menjadi 4KiB
  • Dengan memakai union agar tidak menyimpan sent_ns dan received_ns sekaligus, lalu hanya menyisakan latensi setelah paket diterima, ukuran array turun menjadi 8KiB
  • Presisi diubah dari nanodetik menjadi satuan 100 mikrodetik dan received dijadikan bitfield, tetapi padding struct membuat penghematan tambahan tidak terjadi
  • Dengan mengganti alamat sumber menggunakan sebagian makna identifier ICMP sebagai penghitung 4-bit, ukuran struct turun menjadi 8 byte dan array 512 elemen menjadi 4KiB
  • Aplikasi sebenarnya tidak memiliki keterbatasan memori, jadi tidak ada kebutuhan praktis, tetapi ini menjadi eksperimen optimisasi yang sampai memperhitungkan penempatan field dan biaya akses bit

Menetapkan masalah: cara menyimpan catatan ping

  • Sistem pemantauan konektivitas mengirim ICMP Echo Request ke beberapa server dan mengamati rata-rata latensi serta packet loss pada interval 1 menit, 5 menit, dan 15 menit
  • Cara penyimpanan yang pertama terpikir adalah ring buffer berisi 512 entri, dan setiap entri menyimpan waktu kirim, waktu terima, alamat sumber, nomor urut, serta status diterima atau tidak
  • Ukuran awal array struct pings_rb[512] terukur sebesar 12KiB
struct ping_timestamp {
    uint64_t sent_ns;
    uint64_t received_ns;
    in_addr_t source_addr;
    uint16_t seq_no;
    bool received;
};

Penghematan pertama: menggabungkan waktu kirim dan waktu berlalu dengan union

  • Nilai yang sebenarnya ingin disimpan setelah paket diterima adalah latensi received - sent, sehingga tidak perlu menyimpan waktu kirim dan waktu berlalu secara bersamaan
  • Struct yang menggabungkan sent_ts dan elapsed_ts dalam union memakai slot yang sama sebagai waktu kirim sebelum diterima, lalu sebagai waktu berlalu setelah diterima
  • Setelah perubahan ini, ukuran array 512 elemen turun dari 12KiB menjadi 8KiB
struct ping_timestamp_2 {
    union {
        uint64_t sent_ts;
        uint64_t elapsed_ts;
    };
    in_addr_t source_addr;
    uint16_t seq_no;
    bool received;
};

Percobaan kedua: mengurangi presisi dan memakai bitfield

  • Waktu ping diukur dalam puluhan, ratusan, atau ribuan milidetik, jadi tidak perlu menyimpan seluruh presisi nanodetik
  • Jika satuan waktu diubah menjadi 100 mikrodetik, yaitu 0,1ms, maka 43 bit cukup untuk melacak ping hingga 20 tahun
  • Memakai 8 bit untuk nilai benar/salah pada received terasa berlebihan, jadi bitfield diterapkan
  • Namun ukuran array ping_timestamp_3 tetap 8KiB, sehingga tidak ada penghematan tambahan
struct ping_timestamp_3 {
    uint64_t sent_or_elapsed_ts: 43;
    uint64_t received: 1;
    uint64_t seq_no: 16;
    in_addr_t source_addr;
};

Ukuran tidak mengecil karena padding struct

  • ping_timestamp_2 memiliki byte padding di bagian akhir untuk memenuhi kebutuhan alignment
  • ping_timestamp_3 menaruh waktu, status diterima, dan nomor urut pada 8 byte pertama, tetapi masih menyisakan alamat sumber dan padding di belakangnya
  • Walau bitfield sudah diterapkan, masih ada 36 bit padding sehingga ukuran keseluruhan struct tidak berkurang
  • Sekadar mengecilkan bool menjadi satu bit tidak otomatis menyelesaikan masalah tata letak memori dan alignment

Menghapus alamat sumber dan memakai penghitung 4-bit

  • Karena produk berjalan di jaringan data seluler dan alamat sumber sering berubah, struct lama menyimpan alamat sumber
  • Saat alamat berubah, nomor urut juga di-reset, dan di masa lalu pernah ada paket dengan alamat sumber berbeda tetapi nomor urut sama yang diproses secara bersamaan
  • ICMP Echo Request memiliki field identifier 16-bit yang dapat dipakai aplikasi untuk mengenali paket yang dikirimnya sendiri
  • Karena tidak perlu memakai seluruh 16 bit, 4 bit yang tersisa dipakai sebagai rolling counter yang bertambah saat alamat sumber berubah
  • Penghitung ini dinaikkan mengikuti perubahan alamat sumber yang dipantau di bagian lain aplikasi
struct ping_timestamp {
    uint64_t elapsed_or_sent_ts : 43;
    uint64_t received : 1;
    uint64_t counter: 4;
    uint64_t seq_no: 16;
};

Hasil akhir dan tata letak field

  • Struct final menghapus field alamat sumber dan memuat waktu, status diterima, penghitung, serta nomor urut di dalam 64 bit
  • Ukuran array ring buffer 512 elemen menjadi 4KiB sehingga menyusut menjadi satu halaman data
  • Total penghematan mencapai 8KiB dibanding ukuran awal 12KiB
  • Urutan field disesuaikan agar seq_no sejajar pada batas 16-bit, sehingga saat dimuat dapat dibaca dengan satu instruksi ldrh tanpa shift
  • Saat membaca elapsed_or_sent_ts, yang dibutuhkan hanya masking

Optimisasi tambahan: mengurangi biaya akses bit penerimaan

  • Pembaruan 2025-06-21 menyebutkan bahwa menukar urutan received dan counter membuat akses bit received hanya membutuhkan shift tanpa mask
  • Perubahan ini membuat akses received lebih murah, tetapi menambah biaya saat membaca counter karena bit received harus dihilangkan dengan mask
  • Pembaruan 2025-06-22 memanfaatkan kondisi bahwa counter hanya dibaca saat received bernilai true
  • Dengan membalik makna received menjadi not_received, di dalam kondisi yang memeriksa apakah not_received bernilai 0, mask pada counter sepenuhnya dihilangkan oleh compiler
struct ping_timestamp {
    uint64_t elapsed_or_sent_ts : 43;
    uint64_t counter: 4;
    uint64_t not_received : 1;
    uint64_t seq_no: 16;
};

Kesimpulan

  • Hasil optimisasi memang menurunkan penggunaan memori dari 12KiB menjadi 4KiB, tetapi aplikasi itu sendiri tidak dibatasi oleh memori
  • Terlepas dari kebutuhan praktisnya, ini menjadi eksperimen untuk menelaah layout struct, padding, bitfield, dan biaya akses pada level instruksi
  • Pada komentar terakhir juga dijelaskan bahwa istilah “masalah” dipakai secara longgar, dan bahkan benchmark pun tidak dilakukan

1 komentar

 
GN⁺ 5 jam lalu
Pendapat Lobste.rs
  • Kalau suatu hari memikirkan masalah seperti ini sudah tidak terasa menyenangkan lagi, menurut saya itulah hari untuk berhenti ngoding

  • Optimisasi prematur selalu menyenangkan
    Yang biasanya tidak menyenangkan adalah menangani akibatnya setelah sadar kenapa optimisasi itu prematur

    • Betul. Kita tahu itu bisa jadi bumerang nanti dan seharusnya menahan diri, tapi tetap saja dikerjakan karena seru
  • Bagian yang memakai 43 bit untuk timestamp agak membingungkan. Sepertinya 24 bit sudah cukup
    Dari penyebutan ring buffer berukuran 512, kelihatannya ping baru dikirim setiap 2 detik dan yang dilacak adalah ping selama 17 menit 4 detik terakhir
    Sebagai langkah pertama, bisa pakai delta encoding terhadap timer/urutan ideal. Waktu pengiriman terakhir tinggal dinaikkan 2 detik dan lihat indeks ring buffer, jadi mudah mengetahui kapan paket seharusnya dikirim; lalu cukup catat apakah paket terkirim tepat waktu, terlambat 0,1 ms, terlambat 2,3 ms, dan seterusnya
    Waktu berlalu juga rasanya tidak perlu melebihi 17 menit 4 detik karena setelah itu ping akan kedaluwarsa. 512 × 2s = 10,240,000 × 100μs, jadi pada presisi itu sekitar 23,3 bit sudah cukup, dan kalau mau bisa dibulatkan ke 24 bit. Sisa sekitar 6.536.216 pola bit tidak valid mungkin juga bisa dipakai untuk hal lain
    Bonusnya, dengan 24 bit kita bisa menaikkan presisi “terkirim” jauh lebih tinggi untuk mengurangi galat kuantisasi. Bahkan pada presisi mikrodetik pun ping masih bisa terlambat sampai 16 detik, jadi tampaknya masih sangat longgar
    Saya tidak tahu apakah mengecilkan sampel dari 64 bit ke 48 bit akan membantu atau justru mengganggu performa. Saya juga tidak akan heran kalau hasilnya berbeda di lingkungan x86 dan ARM, baik 32 bit maupun 64 bit

    • AArch64, AArch32 (mungkin sejak ARMv5), dan x86-64 modern semuanya punya instruksi ekstraksi/penyisipan bit field, dan tanpa itu pun biayanya kecil
      Hanya saja, ukuran aslinya pun sudah cukup kecil untuk sangat mudah masuk ke cache data prosesor yang cukup tua, jadi penghematan memori tampaknya tidak akan banyak berpengaruh
  • Saya yakin itulah alasan kita melakukan optimisasi prematur. Ini olahraga untuk bersenang-senang

  • Saat merancang sistem atau bekerja dengan bahasa sistem tingkat rendah, optimisasi prematur sejujurnya adalah salah satu hal favorit saya
    Setidaknya ada harapan bahwa nanti ini akan menghemat waktu dan memori. Hasil di level menengah biasanya cuma sedikit menambah pusing karena harus mencari tahu “kenapa ini dibuat begini?”. Kasus terburuk—dan kadang malah lebih baik—adalah ketika pekerjaan optimisasi saat perancangan jadi terlalu besar sampai proyeknya sendiri tidak jadi dikerjakan. Kita pun menutup program sambil berpikir, “Ah, ini terlalu ruwet, ngapain saya melakukan ini?”