3 poin oleh GN⁺ 2023-09-28 | 1 komentar | Bagikan ke WhatsApp
  • Jawaban Java humanReadableByteCount yang ditulis pada 2010 teridentifikasi dalam riset 2018 sebagai potongan kode Stack Overflow yang paling banyak disalin, tetapi menghasilkan keluaran yang salah pada nilai batas pemformatan ukuran byte
  • Kode ini memilih satuan lewat perhitungan logaritma alih-alih perulangan, dengan memanfaatkan bahwa prefiks seperti kB, MB, GB adalah pangkat dari 1000 atau 1024
  • Bug intinya adalah masalah batas pembulatan yang membuat 999,999 bytes ditampilkan sebagai "1000.0 kB" dalam mode SI, padahal menurut spesifikasi jika rentang angka adalah 1 sampai 999.9 maka hasil yang benar adalah "1.0 MB"
  • Pada nilai yang lebih besar, bug ini bertumpuk dengan batas presisi floating-point pada double, sehingga input 999,949,999,999,999,999 keluar sebagai 1000.0 PB; perbaikannya memerlukan perhitungan ambang, pengurangan skala, koreksi pola bit, dan strictfp
  • Kode akhirnya menangani angka negatif dan Long.MIN_VALUE, tetapi kehilangan kesederhanaan aslinya; menyalin kode dari Stack Overflow juga perlu disertai pengujian edge case dan atribusi sumber

Penyederhanaan yang dituju jawaban tahun 2010

  • Masalahnya adalah memformat jumlah byte menjadi string yang mudah dibaca manusia
    • Contoh: menampilkan 123,456,789 bytes sebagai "123.5 MB"
    • Spesifikasi implisitnya adalah bagian angka pada string hasil berada antara 1 sampai 999.9, lalu diikuti akhiran ukuran yang sesuai
  • Jawaban yang sudah ada memakai pendekatan berbasis perulangan: menelusuri EB, PB, TB, GB, MB, kB, B dari satuan terbesar lalu memilih satuan pertama yang lebih kecil dari jumlah byte
  • Jawaban baru memakai Math.log dan Math.pow untuk mengurangi perulangan dan percabangan
    • Dalam mode SI, satuannya 1000
    • Dalam notasi biner, satuannya 1024
    • Nilai exp = log(bytes) / log(unit) dikonversi ke integer dan dipakai sebagai indeks prefiks
    • Prefiks yang dipakai adalah "kMGTPE" untuk SI, "KMGTPE" untuk biner, dan notasi biner ditambah "i"

Pola penyalinan dan episode OpenJDK

  • Makalah Sebastian Baltes, Usage and Attribution of Stack Overflow Code Snippets in GitHub Projects, menganalisis bagaimana potongan kode Stack Overflow dipakai di proyek GitHub dan apakah sumbernya dicantumkan
  • Metode analisisnya adalah mengekstrak potongan kode dari dump data Stack Overflow lalu mencocokkannya dengan kode di repositori GitHub publik
    • Pertanyaan utamanya adalah apakah atribusi yang sesuai dengan lisensi CC BY-SA 3.0 milik Stack Overflow dipatuhi
    • Hasilnya, sebagian besar pengguna tidak menyertakan atribusi yang semestinya
  • Jawaban dengan ID 3758880 berada di urutan teratas tabel makalah itu, dan saat itu telah ditonton ratusan ribu kali serta mendapat lebih dari 1.000 upvote
  • Jika mencari humanReadableByteCount di GitHub, akan muncul ribuan kasus penggunaan; di repositori lokal, ini bisa dicek dengan perintah berikut
git grep humanReadableByteCount
  • Kecocokan juga ditemukan di repositori OpenJDK
    • Kode tersebut tidak menyertakan atribusi, dan lisensi OpenJDK tidak kompatibel dengan CC BY-SA 3.0
    • Sebastian Baltes bertanya di mailing list pengembang OpenJDK apakah kode itu disalin dari Stack Overflow ke OpenJDK, atau sebaliknya
    • Penulis jawaban itu belum bergabung ke Oracle sebelum commit tersebut digabungkan, dan juga tidak berkontribusi pada patch itu
    • Setelah itu, sebuah isu didaftarkan dan kodenya dihapus

Bug pertama: nilai batas dengan deretan 999

  • Masalah yang secara sepintas tampak mencurigakan ternyata bukan penyebab sebenarnya
    • Nilai maksimum long adalah 2^63 - 1, sekitar 9.2 × 10^18, jadi tidak sampai melampaui satuan setelah EB
    • Saat bytes < unit, if pertama menanganinya sehingga exp tidak menjadi 0 dan charAt(exp - 1) tidak gagal
  • Masalah sebenarnya adalah batas pembulatan
    • Input 999,999 bytes dalam mode SI menjadi "1000.0 kB"
    • Jika bagian angka harus berada antara 1 sampai 999.9 sesuai spesifikasi, hasil yang benar adalah "1.0 MB"
  • Pada saat tulisan ini dibuat, semua 22 jawaban yang diposting, termasuk yang memakai Apache Commons dan library Android, memiliki bug ini atau variasinya
  • Kunci perbaikannya adalah ambang batas yang menentukan kapan eksponen exp dinaikkan ke satuan berikutnya
    • Titik perubahan dari k ke M adalah 999,950, yaitu saat nilainya lebih dekat ke 1 MB daripada 999.9 k
    • Titik perubahan dari M ke G adalah 999,950,000
    • Dalam mode biner, ambangnya bukan bilangan bulat sehingga perlu ceil
if (bytes >= Math.ceil(Math.pow(unit, exp) * (unit - 0.05)))
    exp++;

Bug kedua: batas presisi double

  • Bahkan setelah koreksi di atas diterapkan, input 999,949,999,999,999,999 masih ditampilkan sebagai 1000.0 PB, padahal hasil yang benar adalah 999.9 PB
  • Penyebabnya bukan rumus matematikanya, melainkan batas presisi double
    • Dalam representasi IEEE 754, nilai floating-point yang dekat 0 sangat rapat, tetapi pada nilai besar jaraknya menjadi sangat renggang
    • Pada double yang sangat besar, mengurangkan Long.MAX_VALUE pun bisa tidak mengubah nilainya
double a = Double.MAX_VALUE;
double b = a - Long.MAX_VALUE;
System.err.println(a == b); // prints true
  • Perhitungan yang bermasalah terjadi di dua tempat
    • Pembagian yang dilakukan pada argumen String.format
    • Perhitungan ambang untuk memutuskan apakah exp perlu dinaikkan
  • Masalah pertama ditangani dengan mengurangi nilai bytes antara ke rentang yang presisinya lebih baik, lalu menyesuaikan exp
    • Asumsinya adalah hasil akhir toh akan dibulatkan, jadi digit yang lebih rendah boleh dibuang
if (exp > 4) {
    bytes /= unit;
    exp--;
}
  • Pada masalah kedua, bit-bit rendah justru penting
    • 999,949,99…9 dan 999,950,00…0 harus diklasifikasikan ke eksponen yang berbeda
    • Total ambang yang mungkin ada 12 jika menggabungkan SI dan biner, dan hanya satu yang menghasilkan keluaran salah
    • Hasil yang salah dikoreksi dengan mengenali pola bit yang berakhir dengan D00
    • Karena bergantung pada pola bit dari hasil floating-point tertentu, strictfp pun ditambahkan

Input negatif dan kode final

  • Karena Java tidak memiliki long unsigned, penanganan jumlah byte negatif juga ditambahkan
    • Sebelumnya, input -10,000 ditampilkan sebagai -10000 B
    • absBytes diperkenalkan agar perhitungan terkait exp dilakukan berdasarkan nilai absolut
  • Long.MIN_VALUE memerlukan penanganan khusus
    • Karena -Long.MIN_VALUE == Long.MIN_VALUE
    • Maka jika bytes == Long.MIN_VALUE, dipakai Long.MAX_VALUE; selain itu dipakai Math.abs(bytes)
  • Versi final mencakup strictfp, koreksi ambang, penanganan Long.MIN_VALUE, dan pengurangan skala untuk eksponen besar
  • Kode yang semula ingin menghindari perulangan dan percabangan berlebihan akhirnya menjadi lebih sulit dibaca daripada versi awal setelah semua corner case dibenahi
  • Kode terbaru dengan kualitas production dapat dilihat di tulisan terpisah Formatting byte size to human readable format

Pelajaran praktis yang tersisa

  • Potongan kode Stack Overflow bisa saja mengandung bug meskipun memiliki ribuan upvote
  • Kode yang disalin perlu pengujian edge case, terutama untuk kasus batas
  • Aritmetika floating-point sulit ditangani pada nilai batas dan angka besar
  • Saat menyalin kode, atribusi sumber yang tepat diperlukan; jika tidak, ini bisa menjadi masalah nyata

1 komentar

 
GN⁺ 2023-09-28
Pendapat di Hacker News
  • Menarik bahwa jawaban yang memakai nilai hardcoded dan pernyataan if (atau while) semuanya melakukan maksimal 5 perbandingan
    Jika satuannya hanya B, KiB, MiB, GiB, TiB, hingga EiB, ini bisa diselesaikan dengan maksimal 3 pernyataan if. Jika kita mengecek apakah nilainya GiB atau lebih, kita tahu itu bukan B/KiB/MiB, jadi binary search menang
    Bahkan jika diperluas sampai ZiB dan YiB, maksimal 3 perbandingan sudah cukup, sedangkan pendekatan hardcoded bisa sampai maksimal 7. Kalau saya menulis sendiri, saya tidak akan memakai log/pow/floating point karena peluang salahnya terlalu besar; saya mungkin akan meng-hardcode pernyataan if, tetapi dengan binary search

    • Pendekatan binary search bisa saja lebih lambat daripada sekadar melakukan 6 pemeriksaan. Yang terakhir kemungkinan besar hanya melewati 1 branch, dan karena branch sangat lambat, sebaiknya kode dibuat selurus mungkin alurnya
    • Tergantung distribusi input. Jika nilai kecil sangat umum, linear search bisa lebih baik
    • Menurut saya ini penilaian engineering yang buruk. Solusi sederhana mudah ditinjau oleh rekan kerja, kondisi batasnya terlihat jelas, dan mudah memastikan apakah pengujian mencakupnya
      Kode seperti ini berarti melakukan banyak pekerjaan demi memakai kode yang lebih lambat, lebih kompleks, serta lebih sulit diuji dan di-review
  • (2019) Diskusi-diskusi sebelumnya:
    https://news.ycombinator.com/item?id=21693431
    https://news.ycombinator.com/item?id=21698619
    https://news.ycombinator.com/item?id=27533684

  • Saya tidak paham. Kalau ada 7 sufiks, pilih saja yang benar dengan binary search, dan cukup 3 perbandingan. Atau kalau dibuat sederhana pun hanya 6 perbandingan
    Saya tidak mengerti mengapa memakai log() dua kali, pow() sekali, dan ceil() dianggap lebih baik daripada pendekatan sederhana. Bug yang dijelaskan di sini sendiri adalah contoh sempurna dari masalah yang muncul karena berusaha terlalu pintar

    • Penulis tampaknya mengakui bahwa keterbacaannya buruk dan kembali ke pendekatan memakai loop: https://programming.guide/java/formatting-byte-size-to-human...
      Meski begitu, karena mempertimbangkan bug pembulatan, ini sedikit lebih baik daripada contoh kode pertama di artikel asli
    • Penulis juga sejak awal mengatakan bahwa itu sebenarnya tidak lebih baik daripada loop
      Selain itu, 6 perbandingan hanya terjadi pada nilai maksimum, dan dalam penggunaan nyata tampaknya kecil kemungkinan itu terjadi. Jika sebagian besar nilai berada di rentang B atau KB, pendekatan linear bisa lebih baik
  • Ini memang promosi tanpa malu-malu, tetapi alih-alih menyalin dari S/O, jika ingin memformat ukuran ke bentuk yang mudah dibaca manusia dengan cepat dan akurat, Anda juga bisa memakai library open source kami, PrettySize. Ada versi untuk Rust [0] dan .NET [1], dan library ini juga membuat operasi logika yang type-safe untuk ukuran file menjadi aman dan mudah
    Cuplikan S/O memang 4 baris, tetapi library-library ini jauh lebih komprehensif dan mencakup pengujian, opsi format output, konversi ukuran, dan sebagainya
    [0]: https://github.com/neosmart/prettysize-rs
    [1]: https://github.com/neosmart/PrettySize.net

    • Budaya mengganti solusi 4 baris dengan library besar seperti inilah yang melahirkan left-pad
  • Ini murni rasa penasaran, tapi apakah memang cukup banyak developer yang begitu saja menyalin kode yang tidak bisa dipercaya dari Stack Overflow lalu menempelkannya ke aplikasi?
    Dugaan bahwa orang-orang sekadar menyalin dari Stack Overflow memang terkenal, tetapi sampai melihat seseorang benar-benar melakukannya, saya menganggapnya lebih seperti lelucon. Saya juga memakai Stack Overflow sebagai titik awal saat memecahkan masalah di area yang belum saya kuasai, tetapi saya tidak pernah menyalin kode begitu saja
    Biasanya potongan kode tidak melakukan persis hal yang saya butuhkan, jadi saya harus melihat API dan membuat solusi sendiri berdasarkan pendekatan yang dijelaskan. Khususnya di Python, Stack Overflow sering menunjukkan arah ke API niche yang berguna

    • Dulu saya pernah bekerja dengan developer yang, begitu melihat jawaban, tidak ada yang bisa mencegahnya menyalin ke dalam kode. Ia bahkan tidak membaca pertanyaannya untuk memastikan apakah itu sama dengan masalah yang ia alami, dan tidak membaca jawabannya juga
      Secara harfiah: Google → klik tautan Stack Overflow pertama yang terlihat → salin/tempel blok kode pertama yang terlihat, dan kadang bahasanya pun berbeda. Saat pair programming, saya harus merebut perangkat input secara fisik. Kalau dibilang salah, sebelum kalimat saya selesai ia sudah menempelkan potongan kode kedua di halaman itu, dan ia cepat secara aneh
      Memang ini kasus ekstrem, tetapi ada banyak developer dengan pola pikir “butuh kode; ada kode di Stack Overflow; beres!” tanpa memikirkan sama sekali apakah itu solusi yang tepat
    • Itu benar-benar terjadi, dan makin sering terjadi untuk hal-hal yang terasa berada di luar cakupan bagian program yang saya pedulikan
      Bagaimanapun, kita selalu memakai kode library buatan orang asing untuk bagian pekerjaan plumbing yang sebenarnya tidak terlalu kita pedulikan. Kalau ingin menggali dan memahaminya, kemungkinan besar kita akan menulisnya sendiri, tetapi kalau bagian ini ingin dibiarkan “pokoknya jalan” agar proyek bisa terus berlanjut, jadinya pengembangan yang digerakkan oleh error compiler
    • Karena alasan yang disebut penulis, saya hampir tidak pernah menyalin/menempel apa adanya. Sebaliknya saya berusaha memahami solusinya, dan kalau perlu menyalinnya dengan mengetik manual baris demi baris sampai benar-benar paham, lalu melakukan refactoring dari situ
      Nama variabel juga saya ubah. Karena sering sekali ada terlalu banyak foo, bar, baz sehingga sulit dibaca manusia. Kalau menemui masalah yang sama lagi, lebih mudah juga mengingat apa yang saya lakukan dibanding jika menyalin secara buta
    • Orang-orang memang melakukannya. Setelah melihat begitu banyak kode dan konfigurasi TLS yang salah dari Stack Overflow, saya jadi cukup yakin bahwa sebagian besar sistem berjalan tanpa memverifikasi sertifikat dengan benar
    • Sepertinya Anda belum pernah merasakan nikmatnya bekerja di codebase yang dibuat oleh anak-anak 23 tahun yang minum Adderall
  • Saya tidak mengerti kenapa memakai log floating point padahal yang dibutuhkan adalah log 2
    Kalau tidak ada yang saya lewatkan, ekspresi di bawah ini memberikan floor(log2(value)) secara tepat untuk bilangan positif yang lebih kecil dari 2^63 byte, dan jauh lebih cepat:
    Long.bitCount( (Long.highestOneBit(value) << 1) - 1) - 1

    • Karena satuan yang “umum” adalah pangkat 10, cara ini tidak tepat
  • Begitu melihat potongan kode itu, saya langsung melihat operasi log floating point dan pembagian terhadap integer, jadi saya menganggapnya sebagai kode yang terlalu pintar dan pada dasarnya rawan bug, lalu langsung membuangnya dari kepala saya

    • Itu pada dasarnya inti tulisan tersebut
  • Rantai pengetahuan turun sampai ke dasar. Ini menunjukkan betapa sulitnya memasukkan kembali bahkan pengetahuan yang sangat kecil begitu sudah dikeluarkan
    Di saat Stack Exchange cepat kehilangan kontributor aktif, saya penasaran apa yang dibutuhkan untuk memperbaiki jawaban cepat-cepat tembak yang belakangan terbukti meleset. Dan saya juga penasaran apa artinya bagi pengetahuan kolektif kita ketika jawaban-jawaban yang “sedikit salah” seperti ini makin mengeras di riwayat pencarian dan semakin jauh ke dalam sejarah LLM

  • Saya jadi teringat masa pelatihan dasar militer. Para instruktur sengaja memberi para rekrutan tugas yang tidak ada seorang pun tahu cara melakukannya, tanpa petunjuk, lalu pergi
    Lalu selalu ada seseorang yang mulai dengan cara yang salah, dan sisanya semua ikut orang itu

    • Saya penasaran apakah ini diperparah oleh kecenderungan manusia yang tidak ingin terlihat lebih buruk daripada orang lain. Ini bisa berujung pada hasil bodoh, ketika orang pintar pun mengikuti ide buruk atau ide tergesa-gesa
      Hal serupa juga terjadi dalam prakiraan ekonomi publik. Orang yang salah sendirian ketika orang lain benar diperlakukan jauh lebih keras daripada orang yang salah bersama-sama dengan semua orang
    • Apa tujuan latihan itu?
  • Saya tidak selalu menganggap error floating point dalam algoritme seperti ini sebagai “cacat”. Jika kodenya mendefinisikan solusi yang benar secara logis dan matematis, menurut saya itu sendiri sudah “benar”
    Menangani error floating point adalah satu tingkat di atasnya, dan hanya dilakukan ketika memang penting dalam praktik. Saya bisa membayangkan bahasa pemrograman masa depan yang sempurna, tempat error floating point tidak ada sehingga tidak perlu dipertimbangkan; 99% algoritme saya seolah menargetkan bahasa seperti itu