Sepotong kode paling populer dalam sejarah StackOverflow memiliki bug!

Sebuah studi baru-baru ini, "Menggunakan dan menghubungkan potongan kode Stack Overflow dalam proyek GitHub," tiba-tiba menemukan bahwa sebagian besar waktu dalam proyek sumber terbuka, jawaban saya ditulis hampir sepuluh tahun yang lalu. Ironisnya, ada bug.

Sekali waktu ...


Kembali pada tahun 2010, saya duduk di kantor saya dan melakukan omong kosong: Saya menyukai golf kode dan menambahkan peringkat ke Stack Overflow.

Pertanyaan berikut ini menarik perhatian saya: bagaimana cara menampilkan jumlah byte dalam format yang dapat dibaca? Artinya, bagaimana mengkonversi sesuatu seperti 123456789 byte menjadi "123,5 MB".


Antarmuka 2010 yang bagus, terima kasih The Wayback Machine

Secara implisit, hasilnya adalah angka antara 1 dan 999.9 dengan unit yang sesuai.

Sudah ada satu jawaban dengan satu lingkaran. Idenya sederhana: periksa semua derajat dari unit terbesar (EB = 10 18 byte) ke terkecil (B = 1 byte) dan terapkan yang pertama, yang kurang dari jumlah byte. Dalam kode pseudo, tampilannya seperti ini:

suffixes = [ "EB", "PB", "TB", "GB", "MB", "kB", "B" ] magnitudes = [ 10^18, 10^15, 10^12, 10^9, 10^6, 10^3, 10^0 ] i = 0 while (i < magnitudes.length && magnitudes[i] > byteCount) i++ printf("%.1f %s", byteCount / magnitudes[i], suffixes[i]) 

Biasanya, dengan jawaban yang benar dengan peringkat positif, sulit untuk mengejarnya. Di Stack Overflow, ini disebut masalah penembak tercepat di Barat . Tetapi di sini jawabannya memiliki beberapa kekurangan, jadi saya masih berharap untuk mengatasinya. Setidaknya kode dengan loop dapat sangat dikurangi.

Nah ini aljabar, semuanya sederhana!


Kemudian saya sadar. Awalannya adalah kilo-, mega-, giga-, ... - tidak lebih dari derajat 1000 (atau 1024 dalam standar IEC), sehingga awalan yang benar dapat ditentukan menggunakan logaritma, dan bukan siklusnya.

Berdasarkan ide ini, saya menerbitkan yang berikut:

 public static String humanReadableByteCount(long bytes, boolean si) { int unit = si ? 1000 : 1024; if (bytes < unit) return bytes + " B"; int exp = (int) (Math.log(bytes) / Math.log(unit)); String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i"); return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } 

Tentu saja, ini tidak terlalu mudah dibaca, dan log / pow lebih rendah efisiensinya daripada opsi lain. Tapi tidak ada loop dan hampir tidak ada percabangan, jadi hasilnya cukup indah, menurut saya.

Matematika itu sederhana . Jumlah byte dinyatakan sebagai byteCount = 1000 s , di mana s mewakili derajat (dalam notasi biner, basisnya adalah 1024.) Solusi s memberikan s = log 1000 (byteCount).

Tidak ada log ekspresi sederhana 1000 di API, tetapi kami dapat mengungkapkannya dalam bentuk logaritma natural sebagai berikut s = log (byteCount) / log (1000). Lalu kami mengonversi s ke int, jadi jika, misalnya, kami memiliki lebih dari satu megabita (tetapi tidak satu gigabita penuh), maka MB akan digunakan sebagai unit pengukuran.

Ternyata jika s = 1, maka dimensinya adalah kilobyte, jika s = 2 - megabita dan seterusnya. Bagilah byteCount dengan 1000 s dan tampar huruf yang sesuai ke dalam awalan.

Yang tersisa hanyalah menunggu dan melihat bagaimana masyarakat memahami jawabannya. Saya tidak dapat berpikir bahwa potongan kode ini akan menjadi yang paling banyak beredar dalam sejarah Stack Overflow.

Studi Atribusi


Maju cepat ke 2018. Mahasiswa pascasarjana Sebastian Baltes menerbitkan sebuah artikel di jurnal ilmiah Empirical Software Engineering berjudul "Menggunakan dan Mengaitkan Potongan Kode Stack Overflow dalam Proyek GitHub" . Topik penelitiannya adalah seberapa besar lisensi Stack Overflow CC BY-SA 3.0 dihormati, yaitu, yang penulis tunjukkan ke tautan Stack Overflow sebagai sumber kode.

Untuk analisis, potongan kode diekstraksi dari dump Stack Overflow dan dipetakan ke kode di repositori GitHub publik. Kutipan dari abstrak:

Kami menyajikan hasil studi empiris skala besar yang menganalisis penggunaan dan atribusi fragmen non-sepele kode Java dari jawaban SO dalam proyek GitHub (GH) publik.

(Spoiler: tidak, sebagian besar programmer tidak mematuhi persyaratan lisensi).

Artikel memiliki tabel seperti itu:



Jawaban di atas dengan pengidentifikasi 3758880 ternyata menjadi jawaban yang saya posting delapan tahun lalu. Saat ini, ia memiliki lebih dari seratus ribu pandangan dan lebih dari seribu plus.

Pencarian cepat di GitHub benar-benar menghasilkan ribuan repositori dengan kode humanReadableByteCount .



Cari fragmen ini di repositori Anda:

 $ git grep humanReadableByteCount 

Sebuah cerita lucu , ketika saya mengetahui tentang penelitian ini.

Sebastian menemukan kecocokan dalam repositori OpenJDK tanpa atribusi apa pun, dan lisensi OpenJDK tidak kompatibel dengan CC BY-SA 3.0. Pada milis jdk9-dev, ia bertanya: apakah kode Stack Overflow disalin dari OpenJDK atau sebaliknya?

Yang lucu adalah saya hanya bekerja di Oracle, dalam proyek OpenJDK, jadi mantan kolega dan teman saya menulis yang berikut ini:

Hai

Mengapa tidak bertanya kepada penulis postingan ini langsung di SO (aioobe)? Dia adalah anggota OpenJDK dan bekerja di Oracle ketika kode ini muncul di repositori sumber OpenJDK.

Oracle menangani masalah ini dengan sangat serius. Saya tahu bahwa beberapa manajer merasa lega ketika mereka membaca jawaban ini dan menemukan "pelakunya".

Lalu Sebastian menulis kepada saya untuk mengklarifikasi situasi, yang saya lakukan: kode ini ditambahkan sebelum saya bergabung dengan Oracle dan saya tidak ada hubungannya dengan komit. Lebih baik tidak bercanda dengan Oracle. Beberapa hari setelah tiket dibuka, kode ini dihapus .

Bug


Saya yakin Anda sudah memikirkan hal itu. Apa jenis kesalahan dalam kode?

Sekali lagi:

 public static String humanReadableByteCount(long bytes, boolean si) { int unit = si ? 1000 : 1024; if (bytes < unit) return bytes + " B"; int exp = (int) (Math.log(bytes) / Math.log(unit)); String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i"); return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } 

Apa saja pilihannya?

Setelah exabytes (10 18 ) adalah zettabytes (10 21 ). Mungkin jumlah yang sangat besar akan melampaui kMGTPE? Tidak. Nilai maksimum adalah 2 63 -1 ≈ 9,2 × 10 18 , jadi tidak ada nilai yang akan melampaui exabytes.

Mungkin kebingungan antara unit SI dan sistem biner? Tidak. Ada kebingungan dalam versi pertama jawaban, tetapi itu diperbaiki cukup cepat.

Mungkin exp berakhir dengan zeroing, menyebabkan charAt (exp-1) macet? Tidak juga. Pernyataan if pertama mencakup kasus ini. Nilai exp akan selalu minimal 1.

Mungkin beberapa kesalahan pembulatan yang aneh dalam ekstradisi? Yah, akhirnya ...

Banyak sembilan


Solusinya bekerja sampai mendekati 1 MB. Ketika "1000,0 kB" byte ditentukan sebagai input, hasilnya (dalam mode SI) adalah "1000,0 kB" . Meskipun 999.999 lebih dekat ke 1000 × 1000 1 daripada ke 999.9 × 1000 1 , 1000 penanda dilarang oleh spesifikasi. Hasil yang benar adalah "1.0 MB" .

Dalam pembelaan saya, saya dapat mengatakan bahwa pada saat penulisan, kesalahan semacam itu ada di semua 22 jawaban yang dipublikasikan, termasuk Apache Commons dan perpustakaan Android.

Bagaimana cara memperbaikinya? Pertama-tama, kami mencatat bahwa eksponen (exp) harus berubah dari 'k' ke 'M' segera setelah jumlah byte lebih dekat ke 1 × 1.000 2 (1 MB) daripada ke 999.9 × 1000 1 (999.9 k ) Ini terjadi pada 999.950. Demikian juga, kita harus beralih dari 'M' ke 'G' ketika kita melewati 999.950.000 dan seterusnya.

Kami menghitung ambang ini dan meningkatkan exp jika bytes lebih besar:

 if (bytes >= Math.pow(unit, exp) * (unit - 0.05)) exp++; 

Dengan perubahan ini, kode berfungsi dengan baik hingga jumlah byte mendekati 1 EB.

Lebih banyak Nines


Saat menghitung 999 949 999 999 999 999 999, kodenya memberikan 1000.0 PB , dan hasil yang benar adalah 999.9 PB . Secara matematis, kodenya akurat, jadi apa yang terjadi di sini?

Sekarang kita dihadapkan pada kendala double .

Pengantar aritmatika floating point


Menurut spesifikasi IEEE 754, nilai floating point mendekati nol memiliki representasi yang sangat padat, sedangkan nilai besar memiliki representasi yang sangat jarang. Bahkan, setengah dari semua nilai adalah antara -1 dan 1, dan ketika datang ke angka besar, nilai ukuran Long.MAX_VALUE tidak berarti apa-apa. Dalam arti harfiah.

 double l1 = Double.MAX_VALUE; double l2 = l1 - Long.MAX_VALUE; System.err.println(l1 == l2); // prints true 

Lihat "Bit Poin Mengambang" untuk detailnya.

Masalahnya diwakili oleh dua perhitungan:

  • Divisi dalam String.format dan
  • Ambang ekspansi exp

Kita bisa beralih ke BigDecimal , tapi itu membosankan. Selain itu, masalah juga muncul di sini, karena API standar tidak memiliki logaritma untuk BigDecimal .

Mengurangi nilai antara


Untuk mengatasi masalah pertama, kita dapat mengurangi nilai bytes ke rentang yang diinginkan, di mana akurasi lebih baik, dan menyesuaikan exp . Bagaimanapun, hasil akhir dibulatkan, jadi tidak masalah bahwa kita membuang angka yang paling tidak signifikan.

 if (exp > 4) { bytes /= unit; exp--; } 

Pengaturan bit paling tidak signifikan


Untuk menyelesaikan masalah kedua , bit paling tidak penting penting bagi kami (99994999 ... 9 dan 99995000 ... 0 harus memiliki derajat yang berbeda), jadi kami harus menemukan solusi yang berbeda.

Pertama, perhatikan bahwa ada 12 nilai ambang yang berbeda (6 untuk setiap mode), dan hanya satu yang mengarah ke kesalahan. Hasil yang salah dapat diidentifikasi secara unik karena berakhir pada D00 16 . Jadi Anda bisa memperbaikinya secara langsung.

 long th = (long) (Math.pow(unit, exp) * (unit - 0.05)); if (exp < 6 && bytes >= th - ((th & 0xFFF) == 0xD00 ? 52 : 0)) exp++; 

Karena kami bergantung pada pola bit tertentu dalam hasil floating point, kami menggunakan pengubah ff ketat untuk memastikan bahwa kode bekerja secara independen dari perangkat keras.

Nilai input negatif


Tidak jelas dalam keadaan apa sejumlah byte negatif mungkin masuk akal, tetapi karena Java tidak memiliki long tidak ditandatangani, yang terbaik adalah menangani opsi ini. Saat ini, input seperti -10000 B menghasilkan -10000 B

Mari kita menulis absBytes :

 long absBytes = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes); 

Ekspresi ini sangat verbose karena -Long.MIN_VALUE == Long.MIN_VALUE . Sekarang kita melakukan semua perhitungan exp menggunakan absBytes bukan bytes .

Versi final


Ini adalah versi terakhir dari kode, disingkat dan diringkas dalam semangat versi aslinya:

 // From: https://programming.guide/the-worlds-most-copied-so-snippet.html public static strictfp String humanReadableByteCount(long bytes, boolean si) { int unit = si ? 1000 : 1024; long absBytes = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes); if (absBytes < unit) return bytes + " B"; int exp = (int) (Math.log(absBytes) / Math.log(unit)); long th = (long) (Math.pow(unit, exp) * (unit - 0.05)); if (exp < 6 && absBytes >= th - ((th & 0xfff) == 0xd00 ? 52 : 0)) exp++; String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); if (exp > 4) { bytes /= unit; exp -= 1; } return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } 

Perhatikan bahwa ini dimulai sebagai upaya untuk menghindari loop dan percabangan yang berlebihan. Tetapi setelah menghaluskan semua situasi perbatasan, kode tersebut menjadi lebih mudah dibaca daripada versi aslinya. Secara pribadi, saya tidak akan menyalin fragmen ini dalam produksi.

Untuk versi terbaru dari kualitas produksi, lihat artikel terpisah: "Memformat ukuran byte dalam format yang dapat dibaca . "

Temuan Kunci


  • Mungkin ada kesalahan dalam jawaban untuk Stack Overflow, bahkan jika mereka memiliki ribuan nilai tambah.
  • Periksa semua kasus batas, terutama dalam kode dengan Stack Overflow.
  • Aritmatika titik mengambang rumit.
  • Pastikan untuk menyertakan atribusi yang benar saat menyalin kode. Seseorang mungkin membawa Anda ke air bersih.

Source: https://habr.com/ru/post/id478878/


All Articles