Cara menangani kesalahan pada JVM lebih cepat

Ada berbagai cara untuk menangani kesalahan dalam bahasa pemrograman:


  • pengecualian standar untuk banyak bahasa (Java, Scala dan JVM lainnya, python, dan banyak lainnya)
  • kode status atau bendera (Go, bash)
  • berbagai struktur data aljabar, yang nilainya dapat berupa hasil yang berhasil dan deskripsi kesalahan (Scala, haskell dan bahasa fungsional lainnya)

Pengecualian digunakan sangat luas, di sisi lain mereka sering dikatakan lambat. Tetapi lawan dari pendekatan fungsional sering menarik kinerja.


Baru-baru ini, saya telah bekerja dengan Scala, di mana saya dapat menggunakan pengecualian dan berbagai tipe data untuk penanganan kesalahan, jadi saya ingin tahu pendekatan mana yang akan lebih mudah dan lebih cepat.


Kami akan segera membuang penggunaan kode dan bendera, karena pendekatan ini tidak diterima dalam bahasa JVM dan, menurut pendapat saya, terlalu rentan kesalahan (saya minta maaf atas permainan kata-kata). Karenanya, kami akan membandingkan pengecualian dan berbagai jenis ADT. Selain itu, ADT dapat dianggap sebagai penggunaan kode kesalahan dalam gaya fungsional.


UPDATE : pengecualian tanpa jejak tumpukan ditambahkan ke perbandingan


Kontestan


Sedikit lebih banyak tentang tipe data aljabar

Bagi mereka yang tidak terlalu terbiasa dengan ADT ( ADT ) - tipe aljabar terdiri dari beberapa nilai yang mungkin, yang masing-masing dapat menjadi nilai majemuk (struktur, catatan).


Contohnya adalah tipe Option[T] = Some(value: T) | None Option[T] = Some(value: T) | None , yang digunakan sebagai pengganti nol: nilai dari jenis ini dapat berupa Some(t) jika ada nilai, atau None jika tidak.


Contoh lain adalah Try[T] = Success(value: T) | Failure(exception: Throwable) Try[T] = Success(value: T) | Failure(exception: Throwable) , yang menjelaskan hasil perhitungan yang dapat diselesaikan dengan sukses atau dengan kesalahan.


Jadi para kontestan kami:


  • Pengecualian lama yang bagus
  • Pengecualian tanpa jejak tumpukan, karena mengisi jejak tumpukan adalah operasi yang sangat lambat
  • Try[T] = Success(value: T) | Failure(exception: Throwable) Try[T] = Success(value: T) | Failure(exception: Throwable) - pengecualian yang sama, tetapi dalam pembungkus fungsional
  • Either[String, T] = Left(error: String) | Right(value: T) Either[String, T] = Left(error: String) | Right(value: T) - jenis yang berisi hasil atau deskripsi kesalahan
  • ValidatedNec[String, T] = Valid(value: T) | Invalid(errors: List[String]) ValidatedNec[String, T] = Valid(value: T) | Invalid(errors: List[String]) - jenis dari pustaka Kucing , yang dalam hal kesalahan dapat berisi beberapa pesan tentang kesalahan yang berbeda (tidak cukup List digunakan di sana, tetapi itu tidak masalah)

CATATAN pada dasarnya, pengecualian dibandingkan dengan jejak tumpukan, tanpa dan ATD, tetapi beberapa jenis dipilih, karena Scala tidak memiliki pendekatan tunggal dan menarik untuk membandingkan beberapa.


Selain pengecualian, string digunakan untuk menggambarkan kesalahan, tetapi dengan keberhasilan yang sama dalam situasi nyata, kelas yang berbeda akan digunakan ( Either[Failure, T] ).


Masalah


Untuk menguji penanganan kesalahan, kami mengambil masalah penguraian dan validasi data:


 case class Person(name: String, age: Int, isMale: Boolean) type Result[T] = Either[String, T] trait PersonParser { def parse(data: Map[String, String]): Result[Person] } 

yaitu memiliki data mentah Map[String, String] Anda harus mendapatkan Person atau kesalahan jika data tidak valid.


Lempar


Sebuah solusi untuk dahi menggunakan pengecualian (selanjutnya saya hanya akan memberikan fungsi person , Anda dapat melihat kode lengkap di github ):
Throwparser.scala


  def person(data: Map[String, String]): Person = { val name = string(data.getOrElse("name", null)) val age = integer(data.getOrElse("age", null)) val isMale = boolean(data.getOrElse("isMale", null)) require(name.nonEmpty, "name should not be empty") require(age > 0, "age should be positive") Person(name, age, isMale) } 

di sini string , integer dan boolean memvalidasi keberadaan dan format tipe sederhana dan melakukan konversi.
Secara umum, ini cukup sederhana dan dapat dimengerti.


ThrowNST (Tanpa Jejak Jejak)


Kode ini sama seperti pada kasus sebelumnya, tetapi pengecualian digunakan tanpa jejak stack jika memungkinkan: ThrowNSTParser.scala


Coba


Solusi menangkap pengecualian sebelumnya dan memungkinkan menggabungkan hasil via for (jangan bingung dengan loop dalam bahasa lain):
TryParser.scala


  def person(data: Map[String, String]): Try[Person] = for { name <- required(data.get("name")) age <- required(data.get("age")) flatMap integer isMale <- required(data.get("isMale")) flatMap boolean _ <- require(name.nonEmpty, "name should not be empty") _ <- require(age > 0, "age should be positive") } yield Person(name, age, isMale) 

sedikit lebih tidak biasa untuk mata yang rapuh, tetapi karena penggunaan for , ini sangat mirip dengan versi dengan pengecualian, di samping itu, validasi kehadiran bidang dan penguraian jenis yang diinginkan terjadi secara terpisah ( flatMap dapat dibaca di sini saat and then )


Baik


Di sini, tipe Either tersembunyi di belakang alias Result karena jenis kesalahan diperbaiki:
EitherParser.scala


  def person(data: Map[String, String]): Result[Person] = for { name <- required(data.get("name")) age <- required(data.get("age")) flatMap integer isMale <- required(data.get("isMale")) flatMap boolean _ <- require(name.nonEmpty, "name should not be empty") _ <- require(age > 0, "age should be positive") } yield Person(name, age, isMale) 

Karena standar Either seperti Try membentuk monad di Scala, kode keluar persis sama, perbedaannya di sini adalah bahwa string muncul di sini sebagai kesalahan dan pengecualian minimal digunakan (hanya untuk menangani kesalahan ketika mengurai angka)


Divalidasi


Di sini pustaka Kucing digunakan untuk mendapatkan bukan hal pertama yang terjadi, tetapi sebanyak mungkin (misalnya, jika beberapa bidang tidak valid, hasilnya akan berisi kesalahan parsing untuk semua bidang ini)
ValidatedParser.scala


  def person(data: Map[String, String]): Validated[Person] = { val name: Validated[String] = required(data.get("name")) .ensure(one("name should not be empty"))(_.nonEmpty) val age: Validated[Int] = required(data.get("age")) .andThen(integer) .ensure(one("age should be positive"))(_ > 0) val isMale: Validated[Boolean] = required(data.get("isMale")) .andThen(boolean) (name, age, isMale).mapN(Person) } 

kode ini sudah kurang mirip dengan versi aslinya dengan pengecualian, tetapi verifikasi pembatasan tambahan tidak diceraikan dari bidang parsing dan kami masih mendapatkan beberapa kesalahan, bukan satu, sepadan!


Pengujian


Untuk pengujian, kumpulan data dihasilkan dengan persentase kesalahan yang berbeda dan diuraikan dalam masing-masing cara.


Hasil pada semua persentase kesalahan:


Secara lebih rinci, dengan persentase kesalahan yang rendah (waktunya berbeda di sini karena sampel yang lebih besar digunakan):


Jika beberapa bagian dari kesalahan masih merupakan pengecualian dengan jejak tumpukan (dalam kasus kami, kesalahan penguraian nomor akan menjadi pengecualian yang tidak kami kontrol), maka tentu saja kinerja metode penanganan kesalahan "cepat" akan menurun secara signifikan. Validated sangat terpengaruh, karena mengumpulkan semua kesalahan dan sebagai hasilnya menerima pengecualian lambat lebih dari yang lain:


Kesimpulan


Seperti yang ditunjukkan percobaan, pengecualian dengan jejak tumpukan benar-benar sangat lambat (100% kesalahannya adalah perbedaan antara Throw dan Either lebih dari 50 kali!), Dan ketika hampir tidak ada pengecualian, menggunakan ADT memiliki harga. Namun, menggunakan pengecualian tanpa jejak tumpukan sama cepat (dan dengan persentase kesalahan yang rendah lebih cepat) seperti ADT, namun, jika pengecualian tersebut melampaui batas validasi yang sama, melacak sumbernya tidak akan mudah.


Secara total, jika probabilitas pengecualian lebih dari 1%, maka pengecualian tanpa jejak tumpukan bekerja paling cepat, Validated atau reguler. Either hampir sama cepat. Dengan sejumlah besar kesalahan, Either bisa sedikit lebih cepat daripada Validated hanya karena semantik gagal-cepat.


Menggunakan ADT untuk penanganan kesalahan memberikan keuntungan lain dari pengecualian: kemungkinan kesalahan ditransfer ke dalam tipe itu sendiri dan lebih sulit untuk dilewatkan, seperti ketika menggunakan Option bukan nol.

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


All Articles