- 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
Pendapat di Hacker News
Menarik bahwa jawaban yang memakai nilai hardcoded dan pernyataan
if(atauwhile) semuanya melakukan maksimal 5 perbandinganJika 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 menangBahkan 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 pernyataanif, tetapi dengan binary searchKode 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
The most copied StackOverflow snippet of all time is flawed (2019) - https://news.ycombinator.com/item?id=27533684 - Juni 2021, 334 komentar
The most copied StackOverflow snippet of all time is flawed - https://news.ycombinator.com/item?id=21698619 - Desember 2019, 88 komentar
The most copied StackOverflow snippet of all time is flawed - https://news.ycombinator.com/item?id=21693431 - Desember 2019, 3 komentar
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, danceil()dianggap lebih baik daripada pendekatan sederhana. Bug yang dijelaskan di sini sendiri adalah contoh sempurna dari masalah yang muncul karena berusaha terlalu pintarMeski begitu, karena mempertimbangkan bug pembulatan, ini sedikit lebih baik daripada contoh kode pertama di artikel asli
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
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
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
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
Nama variabel juga saya ubah. Karena sering sekali ada terlalu banyak
foo,bar,bazsehingga sulit dibaca manusia. Kalau menemui masalah yang sama lagi, lebih mudah juga mengingat apa yang saya lakukan dibanding jika menyalin secara butaSaya tidak mengerti kenapa memakai log floating point padahal yang dibutuhkan adalah
log 2Kalau 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) - 1Begitu melihat potongan kode itu, saya langsung melihat operasi
logfloating point dan pembagian terhadap integer, jadi saya menganggapnya sebagai kode yang terlalu pintar dan pada dasarnya rawan bug, lalu langsung membuangnya dari kepala sayaRantai 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
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
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