3 poin oleh GN⁺ 2025-05-18 | 1 komentar | Bagikan ke WhatsApp
  • Memindahkan pernyataan if di dalam fungsi ke sisi pemanggil membantu mengurangi kompleksitas kode
  • Dengan memusatkan pemeriksaan kondisi dan penanganan percabangan di satu tempat, duplikasi dan pemeriksaan cabang yang tidak perlu jadi lebih mudah ditemukan
  • Menggunakan refaktorisasi pembubaran enum dapat mencegah kondisi yang sama tersebar di berbagai bagian kode
  • Perulangan for berbasis operasi batch efektif untuk meningkatkan performa dan mengoptimalkan pekerjaan berulang
  • Dengan menggabungkan pola if ke atas, for ke bawah, keterbacaan dan efisiensi kode bisa ditingkatkan sekaligus

Catatan singkat tentang dua aturan yang saling berkaitan

  • Jika ada kondisi if di dalam fungsi, pendekatan yang disarankan adalah mempertimbangkan apakah itu bisa dipindahkan ke sisi pemanggil fungsi
  • Seperti pada contoh, alih-alih memeriksa precondition di dalam fungsi, lebih baik menyerahkan pemeriksaan kondisi tersebut ke sisi pemanggil, atau menjamin precondition melalui tipe (atau assert)
  • Pendekatan menaikkan pemeriksaan precondition (push up) memberi dampak ke seluruh kode, sehingga secara keseluruhan mengurangi jumlah pemeriksaan kondisi yang tidak perlu

Memusatkan alur kontrol dan kondisi

  • Alur kontrol dan pernyataan if adalah penyebab utama kompleksitas kode dan bug
  • Memusatkan kondisi di level atas seperti sisi pemanggil, lalu mengumpulkan penanganan percabangan hanya di satu fungsi, dan menyerahkan pekerjaan nyata ke subrutin yang lurus (straight-line), adalah pola yang bermanfaat
  • Jika percabangan dan alur kontrol terkumpul di satu tempat, percabangan yang duplikat dan kondisi yang tidak perlu jadi lebih mudah dikenali

Contoh:

  • Saat ada if bertingkat di dalam fungsi f, cabang mati (Dead Branch) lebih mudah dikenali
  • Jika percabangan tersebar melalui beberapa fungsi (g, h), hal seperti itu menjadi lebih sulit dipahami

Refaktorisasi pembubaran enum (Dissolving enum Refactor)

  • Jika kode memuat percabangan kondisi yang sama lewat enum atau bentuk lain, kondisi itu bisa ditarik ke level atas agar percabangan dan pekerjaan lebih jelas terpisah
  • Dengan menerapkan cara ini, masalah kondisi yang sama muncul berulang kali di dalam kode dapat dicegah

Contoh:

  • Situasi ketika kondisi percabangan yang sama dinyatakan masing-masing di fungsi f, g, dan enum E
  • Dapat disederhanakan menjadi satu percabangan kondisi di level atas untuk seluruh kode

Pemikiran berorientasi data (Data Oriented Thinking) dan operasi batch

  • Sebagian besar program berjalan dengan banyak objek (entitas). Performa pada jalur kritis (Hot Path) ditentukan oleh pemrosesan banyak objek
  • Dengan memperkenalkan konsep batch dan menjadikan operasi atas kumpulan objek sebagai dasar, sementara operasi objek tunggal ditangani sebagai kasus khusus, hasilnya lebih baik

Contoh:

  • Menjadikan fungsi pemrosesan batch seperti frobnicate_batch(walruses) sebagai dasar

  • Lalu objek individual bisa diubah menjadi kasus khusus yang diproses lewat loop for

  • Pendekatan ini berperan penting dalam optimasi performa; pada pekerjaan berskala besar, biaya awal dapat dikurangi dan fleksibilitas urutan dapat ditingkatkan

  • Pemanfaatan operasi SIMD (struct-of-array dan sejenisnya) juga dimungkinkan, sehingga hanya field tertentu yang diproses sekaligus sebelum pekerjaan keseluruhan dilanjutkan

Contoh praktis dan pola yang direkomendasikan

  • Seperti pada perkalian polinomial berbasis FFT, performa dapat dimaksimalkan dengan memungkinkan perhitungan serentak di banyak titik
  • Aturan menaikkan kondisi dan menurunkan perulangan dapat diterapkan bersamaan

Contoh:

  • Daripada terus memeriksa ekspresi kondisi yang sama di dalam loop, memindahkan kondisi ke luar loop mengurangi percabangan di dalam iterasi dan mempermudah optimasi serta vektorisasi
  • Pendekatan ini menjamin efisiensi tinggi pada data plane sistem berskala besar, seperti dalam desain TigerBeetle

Kesimpulan

  • Dengan menggabungkan pola memindahkan if (kondisi) ke level atas (pemanggil, pengendali) dan for (perulangan) ke level bawah (operasi, pemrosesan data), keterbacaan, efisiensi, dan performa kode semuanya dapat ditingkatkan
  • Berpikir dari sudut pandang ruang vektor abstrak, yaitu operasi pada tingkat himpunan, adalah alat pemecahan masalah yang lebih baik daripada pemrosesan percabangan berulang
  • Singkatnya: if ke atas, for ke bawah!

1 komentar

 
GN⁺ 2025-05-18
Komentar Hacker News
  • Model mental saya yang agak unik adalah bahwa berbagai state atau alur program membentuk struktur pohon. Pernyataan kondisional berperan memangkas cabang pada pohon itu. Saya ingin memangkasnya sedini mungkin agar jumlah cabang yang harus diproses setelahnya berkurang. Saya ingin menghindari situasi di mana semua cabang dievaluasi dan dibereskan satu per satu, lalu pada akhirnya seluruh cabang tetap harus dipotong sekaligus. Dilihat dari sudut pandang yang agak berbeda, kondisional adalah "proses menemukan pekerjaan yang tidak perlu", sedangkan loop adalah "pekerjaan yang sesungguhnya". Pada akhirnya, fungsi yang saya inginkan berbentuk sesuatu yang fokus pada salah satu dari dua hal: menelusuri pohon program, atau mengerjakan pekerjaan nyata
    • Saya ingin mengajukan model saya sendiri. Kelas adalah kata benda, dan fungsi adalah kata kerja
    • Model mental saya menyesuaikan diri dengan dunia tempat kode konkret yang saya tulis berada. Itu bergantung pada karakteristik domain, pola kode yang sudah ada, tahap dalam pipeline data, profil performa, dan sebagainya. Awalnya saya mencoba membuat aturan atau heuristik seperti ini, tetapi setelah banyak menulis kode saya sadar bahwa aturan abstrak seperti ini pada praktiknya tidak terlalu berarti. Sering kali orang malah menetapkan nama fungsi atau satu kata tertentu, lalu aturan itu hanya berlaku di dalam "kode pulau" tersebut, padahal di codebase nyata biasanya ada alasan mengapa fungsi-fungsi itu memang tidak digabung. Sebagai contoh, pembahasan tentang "duplikasi dan kondisi mati" menerapkan aturan yang nyaman karena berasumsi fungsi tersebut hanya dipanggil dari satu tempat. Dalam praktiknya, sering kali ada alasan lain mengapa keduanya dipisahkan
    • Menurut saya ini model yang cukup bagus
  • Aturan yang lebih umum adalah menempatkan kondisional sedekat mungkin dengan sumber input. Intinya adalah mengidentifikasi sedini mungkin titik masuk dari luar ke dalam program (termasuk data yang diambil dari layanan lain), lalu membangun sebanyak mungkin jaminan sebelum mencapai logika inti (terutama sebelum menyentuh bagian yang mahal dalam penggunaan resource). Menyatakan hal itu secara eksplisit di dalam type juga sangat baik
    • Bukankah dengan begitu jadi sulit mengetahui asumsi apa yang dimiliki saat memahami logika inti? Bukankah kita harus menelusuri seluruh rantai pemanggilan kode?
  • Saran "jika ada kondisi if di dalam fungsi, pertimbangkan apakah itu bisa dipindahkan ke sisi pemanggil" punya terlalu banyak kontra-contoh. Kalau sebuah fungsi dipanggil dari 37 tempat, apakah kita harus mengulang if yang sama di setiap pemanggilan? Misalnya, apakah masuk akal menyuruh fungsi seperti getaddrinfo atau EnterCriticalSection memindahkan if seperti itu? Menurut saya, transformasi seperti ini hanya layak dipertimbangkan bila fungsi itu dipanggil dari sekitar dua tempat saja, dan keputusan tersebut berada di luar concern fungsi itu. Salah satu caranya adalah menulis fungsi yang hanya menjalankan kondisional, lalu mendelegasikan ke helper function. Dan ketika perlu memindahkan kondisi ke luar loop, pemanggil bisa langsung memakai helper kondisi tingkat lebih rendah. Namun inti dari pertimbangan ini adalah "optimisasi". Optimisasi sering kali berbenturan dengan desain program yang lebih baik. Bisa jadi desain yang lebih baik justru tidak mengharuskan pemanggil mengetahui kondisi itu. Dilema seperti ini juga sering muncul dalam OOP. Keputusan yang direpresentasikan dengan "if" pada praktiknya kadang diwujudkan sebagai method dispatch. Memindahkan dispatch seperti ini ke luar loop juga bisa berbenturan dengan prinsip desain. Contohnya, saat menggambar gambar pada canvas, lebih baik memanfaatkan method seperti blit daripada berulang kali memanggil putpixel
    • Jika sebuah fungsi dipanggil dari 37 tempat, memang perlu ada refactoring kode. Menjawab pertanyaan itu, ya tergantung situasi. DRY terasa seperti jawaban yang benar, tetapi sebaiknya diputuskan setelah melihat contoh kode nyata. Kalau itu library, ia berada di batas kepemilikan, jadi masing-masing harus mengelola data dan tanggung jawabnya sendiri. Fungsi seperti EnterCriticalSection memang seharusnya melakukan validasi kuat di titik masuknya, termasuk kondisional. Tetapi di kode aplikasi, memindahkan if ke sisi pemanggil bisa saja wajar. Di library atau kode inti, memindahkan alur kontrol ke tepi itu tepat. Di dalam domain yang kita kuasai sendiri, menaruh alur kontrol di tepi memang bagus. Tapi seperti biasa aturan seperti ini hanya idiomatis, jadi harus diterapkan sesuai konteks oleh orang yang bisa menilai situasinya secara masuk akal
  • Contoh refaktor "dissolving enum" pada dasarnya adalah pola polymorphism. Pernyataan match bisa diganti dengan pemanggilan method polimorfik. Tujuan pendekatan ini adalah memisahkan saat keputusan percabangan awal ditentukan dari saat perilaku nyata dijalankan. Pembedaan kasus dibawa oleh objek (di sini nilai enum) atau closure, jadi tidak perlu diulang setiap kali dipanggil. Jika pembedaan kasus berubah, cukup ubah titik percabangannya, dan tempat terjadinya perilaku nyata tidak perlu diubah. Kekurangannya adalah trade-off antara kemudahan melihat langsung cabang perilaku untuk tiap kasus, dan munculnya dependensi pada daftar kasus di level kode
  • Kadang saya justru suka menaruh kondisional di dalam fungsi. Dengan sengaja saya bisa mencegah pemanggil salah dalam urutan pemanggilan fungsi. Misalnya, ketika butuh jaminan idempotensi, kita cek dulu apakah statusnya sudah diproses, kalau belum baru dijalankan. Kalau kondisi ini dipindahkan ke pemanggil, maka idempotensi hanya terjamin jika semua pemanggil menjalankan prosedur itu dengan benar, sehingga abstraksi tidak lagi memberikan jaminan tersebut. Dalam situasi seperti ini saya penasaran bagaimana filosofi ini diterapkan. Contoh lain, saat ingin melakukan serangkaian pengecekan lalu menjalankan pekerjaan di dalam transaksi database, saya bingung pengecekan itu sebaiknya diletakkan di mana
    • Sepertinya Anda sudah menjawab pertanyaan itu sendiri. Jika kondisional dipindahkan ke pemanggil, fungsi itu tidak lagi idempoten dan tentu tidak bisa menjaminnya. Jika Anda menaruh logika manajemen state di setiap fungsi demi menjamin idempotensi, mungkin Anda sedang menulis kode yang cukup aneh, dan itu berarti terlalu banyak business logic ditumpuk ke satu fungsi. Kode idempoten secara garis besar terbagi dua. Pertama, kode yang model data atau operasinya sendiri memang idempoten. Dalam kasus ini kita tidak perlu terlalu peduli dengan urutan pemrosesan. Kedua, membuat abstraksi idempoten untuk operasi bisnis yang lebih kompleks. Ini membutuhkan logika rumit seperti rollback atau atomic apply abstraction, dan kasus seperti ini bukan hal yang bisa begitu saja ditampung secara sederhana dalam satu fungsi
    • Salah satu cara adalah membuat fungsi internal tanpa pengecekan, lalu mengelolanya dengan wrapper function di luar yang melakukan pengecekan terlebih dulu sebelum memanggil fungsi internal
  • Pemindai kompleksitas kode pada akhirnya adalah alat yang cenderung mendorong if ke bawah. Tetapi tulisan ini justru menyarankan kebalikannya: menaikkan if ke atas, yaitu ke fungsi yang lebih tinggi. Dengan begitu, logika percabangan yang kompleks bisa ditangani secara terpusat dalam satu fungsi, dan pekerjaan konkret yang sebenarnya didelegasikan ke subroutine
    • Solusinya adalah memisahkan "keputusan" dan "eksekusi". Ide ini saya pelajari dari Bertrand Meyer. Misalnya if (weShouldDoThis()) { doThis(); } seperti itu, dan jika tiap pengecekan dipisah menjadi fungsi tersendiri, pengujian dan pengelolaan kompleksitas jadi lebih mudah
    • Laporan dari pemindai kode perlu diragukan secara serius. sonarqube dan sejenisnya sering melaporkan "code smell" secara membabi buta, bukan bug nyata. Kalau kita sampai memperbaiki "kode yang sebenarnya tidak bermasalah" dengan gaya seperti ini, risikonya justru menimbulkan bug baru, dan hanya membuang waktu yang seharusnya dipakai untuk menangani masalah yang benar-benar penting
    • Optimisasi seperti ini umumnya hanya berupa "optimum lokal". Artinya, begitu ada requirement baru atau kasus pengecualian ditemukan, logika percabangan jadi dibutuhkan di luar loop. Dalam kondisi seperti itu, kalau percabangan bercampur di dalam dan di luar loop sekaligus, kode jadi sulit dipahami. Kalau Anda yakin kondisi itu hanya dibutuhkan di dalam loop, taruh saja di sana. Kalau tidak, menurut saya lebih baik sejak awal mengambil desain yang sedikit lebih panjang, dan meskipun kodenya lebih verbose, hasilnya lebih mudah dipahami. Saya pernah mengalami ini saat memakai Haskell. Jika kita mengejar bentuk logika yang paling ringkas dan paling optimal sebagai local optimum, sedikit saja requirement berubah maka yang tersisa bukan lagi ekspresi dari niat desain, melainkan sekadar logika, dan perubahan kecil pun bisa menyebabkan code unrolling yang cukup parah
    • Pemindai kompleksitas kode selalu menjadi sumber keluhan bagi saya. Bahkan fungsi besar yang mudah dibaca pun diprotes. Menaruh logika di satu tempat memang memudahkan memahami konteks keseluruhan, tetapi saat memecah fungsi, kita harus berhati-hati agar konteks yang sebenarnya tidak hilang
    • Kemarin di thread tentang LLM ada pembahasan soal "alat tak andal yang tetap diterima para developer". Sekarang saya tahu jawabannya…
  • Dalam beberapa kasus, pendekatannya justru perlu dibalik dan memanfaatkan SIMD. Misalnya pada AVX-512 dan sejenisnya, kode yang bercabang bisa diproses sebagai kode tanpa branch menggunakan vector mask register. Contohnya, if di dalam for lebih mudah dikelola dan lebih efisien untuk akses memori daripada if di luar for. Contoh konkretnya, jika ada operasi +1 untuk bilangan ganjil dan -2 untuk bilangan genap, biasanya setiap iterasi loop harus bercabang, tetapi dengan SIMD kita bisa memproses 16 int sekaligus secara vektor tanpa branch. Jika compiler melakukan vectorization dengan benar, kode asli bisa diubah menjadi versi optimal tanpa branch
    • Menurut saya kode before yang diajukan agak kurang cocok dengan inti argumen tulisan itu, dan justru versi SIMD yang dioptimalkan itulah yang sesuai dengan poin utamanya. Dalam contoh tersebut, if di dalam for adalah percabangan yang bergantung pada data sehingga tidak mudah diangkat ke atas. Kalau algoritmenya berbentuk if (length % 2 == 1) { ... } else { ... } dan hanya memakai kondisi di luar loop, maka kondisi seperti itu memang jelas harus dipindahkan ke atas for. Pada versi SIMD, if bahkan hilang sama sekali, dan pola seperti ini adalah bentuk kode ideal yang kemungkinan juga akan disukai penulis artikel itu
    • Saya juga langsung teringat pada kode yang bercabang berdasarkan nilai elemen dalam loop for. Adakah yang tahu seberapa sulit compiler melakukan auto-vectorization untuk kode seperti ini? Saya penasaran di mana batasnya
  • Secara pribadi saya tidak menganggap ini aturan yang "bagus". Ada kasus yang memang bisa menerapkannya, tetapi konteksnya terlalu bervariasi sehingga sulit menarik kesimpulan yang tegas. Rasanya seperti aturan ejaan bahasa Inggris: terlalu banyak pengecualian sehingga sulit dianggap benar-benar sebagai aturan
  • Tautan diskusi saat itu (2023) (662 poin, 295 komentar) https://news.ycombinator.com/item?id=38282950
  • Saya pernah menemukan gagasan serupa di 99 Bottles of OOP karya Sandi Metz. Ini bukan gaya saya, tetapi saya setuju bahwa menaikkan logika percabangan ke bagian paling atas call stack bisa berguna. Saya sangat merasakannya terutama di codebase yang sebelumnya meneruskan flag melewati banyak layer. https://sandimetz.com/99bottles
    • Saya langsung teringat pada tulisan penulis yang sama, "The Wrong Abstraction". Percabangan di dalam for membentuk abstraksi bahwa "for adalah aturan, percabangan adalah perilaku". Tetapi ketika requirement baru muncul, abstraksi seperti ini runtuh, lalu orang mulai memaksa menambah parameter atau memperbanyak penanganan kasus khusus sehingga kode makin sulit dipahami. Seandainya sejak awal kode ditulis tanpa abstraksi itu, hasilnya mungkin akan lebih jelas dan lebih mudah dirawat. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction