Benchee

Αυτή η μετάφραση είναι πλήρως ενημερωμένη.

Δεν μπορούμε απλά να υποθέτουμε ποιές συναρτήσεις είναι γρήγορες και ποιές αργές - χρειαζόμαστε πραγματικές μετρήσεις όταν είμαστε περίεργοι. Εδώ έχει θέση η συγκριτική αξιολόγηση. Σε αυτό το μάθημα, θα μάθουμε πόσο εύκολο είναι να μετρήσουμε την ταχύτητα του κώδικά μας.

Πίνακας περιεχομένων

Σχετικά με το Benchee

Παρόλο που υπάρχει μια συνάρτηση στην Erlang που μπορεί να χρησιμοποιηθεί για πολύ βασικές μετρήσεις του χρόνου εκτέλεσης μιας συνάρτησης, δεν είναι το ίδιο καλή στη χρήση σε σχέση με μερικά από τα διαθέσιμα εργαλεία και δεν σας δίνει πολλαπλές μετρήσεις για να πάρετε καλά στατιστικά. Για αυτό το λόγο θα χρησιμοποιήσουμε το Benchee. To Benchee μας παρέχει ένα εύρος στατιστικών με εύκολες συγκρίσεις μεταξύ σεναρίων, ένα τρομερό χαρακτηριστικό που μας επιτρέπει να ελέγξουμε διαφορετικές εισόδους στις συναρτήσεις που μετράμε και διάφορους μορφοποιητές για να εμφανίσουμε τα αποτελέσματά μας, καθώς και την ικανότητα να γράψουμε τον δικό μας μορφοποιητή αν το επιθυμούμε.

Χρήση

Για να προσθέσουμε το Benchee στο project μας, προσθέστε το σαν εξάρτηση στο mix.exs αρχείο σας:

defp deps do
  [{:benchee, "~> 0.9", only: :dev}]
end

Τότε καλούμε:

$ mix deps.get
...
$ mix compile

Η πρώτη εντολή θα κατεβάσει και εγκαταστήσει το Benchee. Μπορεί να σας ζητηθεί να εγκαταστήσετε το Hex μαζί με αυτό. Η δεύτερη συντάσσει την εφαρμογή Benchee. Τώρα είμαστε έτοιμοι να γράψουμε την πρώτη μας συγκριτική αξιολόγηση!

Μια σημαντική σημείωση πριν ξεκινήσουμε: Όταν αξιολογούμε, είναι πολύ σημαντικό να μην χρησιμοποιούμε το iex από τη στιγμή που συμπεριφέρεται διαφορετικά και είναι συχνά πιο αργό από τη συμπεριφορά του κώδικά σας στην παραγωγή. Έτσι, ας δημιουργήσουμε ένα αρχείο που θα ονομάσουμε benchmark.exs και σε αυτό το αρχείο ας βάλουμε τον παρακάτω κώδικα:

list = Enum.to_list(1..10_000)
map_fun = fn i -> [i, i * i] end

Benchee.run(%{
  "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
  "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten() end
})

Τώρα για να τρέξουμε την συγκριτική αξιολόγηση, καλούμε:

$ mix run benchmark.exs

Και θα πρέπει να δούμε κάτι σαν την παρακάτω έξοδο στην κονσόλα σας:

Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-4260U CPU @ 1.40GHz
Number of Available Cores: 4
Available memory: 8.589934592 GB
Elixir 1.5.1
Erlang 20.0
Benchmark suite executing with the following configuration:
warmup: 2.00 s
time: 5.00 s
parallel: 1
inputs: none specified
Estimated total run time: 14.00 s


Benchmarking flat_map...
Benchmarking map.flatten...

Name                  ips        average  deviation         median
flat_map           1.03 K        0.97 ms    ±33.00%        0.85 ms
map.flatten        0.56 K        1.80 ms    ±31.26%        1.60 ms

Comparison:
flat_map           1.03 K
map.flatten        0.56 K - 1.85x slower

Φυσικά οι πληροφορίες του συστήματός σας και τα αποτελέσματα μπορεί να διαφέρουν ανάλογα με τα χαρακτηριστικά του μηχανήματος που τρέχετε την συγκριτική αξιολόγηση, αλλά αυτές οι γενικές πληροφορίες θα πρέπει να είναι εκεί.

Με μια πρώτη ματιά, ο τομές Comparison μας δείχνει ότι η εκδοχή μας της map.flatten είναι 1.85 φορές πιο αργή από την flat_map - πολύ χρήσιμο να το γνωρίζουμε! Αλλά ας ρίξουμε μια ματιά και στα άλλα στατιστικά που παίρνουμε:

  • ips - σημαίνει “iterations per second - επαναλήψεις το δευτερόλεπτο”, το οποίο μας λέει πόσο συχνά η δωθείσα συνάρτηση μπορεί να εκτελεστεί σε ένα δευτερόλεπτο. Για αυτή τη μέτρηση, ένας μεγαλύτερος αριθμός είναι καλύτερος.
  • average - είναι ο μέσος χρόνος εκτέλεσης της δωθείσας συνάρτησης. Για αυτή τη μέτρηση, ένας χαμηλότερος αριθμός είναι καλύτερος.
  • deviation - είναι η τυπική απόκλιση, η οποία μας λέει πόσο πολύ αποκλείνουν τα αποτελέσματα κάθε επανάληψης. Εδώ δίνεται σαν ποσοστό επί του μέσου χρόνου εκτέλεσης.
  • median - όταν όλες οι μετρημένες τιμές ταξινομηθούν, αυτή είναι η μεσαία τιμή (ή η μέση τιμή των δύο μεσέων τιμών όταν ο αριθμός δειγμάτων είναι ζυγός). Εξ’ αιτίας περιβαλοντικών ανακολουθιών αυτή η τιμή θα είναι πιο σταθερή από την average, και πιο ικανή να αντικατοπτρίσει την κανονική απόδοση του κώδικά σας στην παραγωγή. Για αυτή τη μέτρηση, ένας χαμηλότερος αριθμός είναι καλύτερος.

Υπάρχουν επίσης άλλα διαθέσιμα στατιστικά, αλλά αυτές οι τέσσερις είναι συχνά οι πιό χρήσιμες και συχνά χρησιμοποιούμενες για συγκριτικές αξιολογήσεις, γιαυτό και εμφανίζονται στον προκαθορισμένο μορφοποιητή. Για να μάθετε περισσότερα για τις άλλες διαθέσιμες μετρήσεις, ελέγξτε την τεκμηρίωση στα hexdocs.

Ρυθμίσεις

Ένα από καλύτερα μέρη του Benchee είναι οι διαθέσιμες επιλογές ρυθμίσεων. Θα δούμε πρώτα τα βασικά από τη στιγμή που δεν απαιτούν παραδείγματα κώδικα, και τότε θα δείξουμε πως να χρησιμοποιούμε ένα από τα καλύτερα χαρακτηριστικά του Benchee - τις εισαγωγές.

Βασικές

Το Benchee δέχεται μια πληθώρα επιλογών ρυθμίσεων. Στην πιο συχνή διεπαφή Benchee.run/2, αυτές περνάνε ως το δεύτερο όρισμα στη μορφή μιας προαιρετικές λίστας λέξεων κλειδί:

Benchee.run(
  %{"example function" => fn -> "hi!" end},
  warmup: 4,
  time: 10,
  inputs: nil,
  parallel: 1,
  formatters: [&Benchee.Formatters.Console.output/1],
  print: [
    benchmarking: true,
    configuration: true,
    fast_warning: true
  ],
  console: [
    comparison: true,
    unit_scaling: :best
  ]
)

Οι διαθέσιμες επιλογές είναι οι ακόλουθες (επίσης τεκμηριωμένες στα hexdocs).

  • warmup - ο χρόνος σε δευτερόλεπτα για τον οποίο ένα σενάριο συγκριτικής αξιολόγησης θα πρέπει να τρέξει χωρίς να υπολογίζεται ο χρόνος πριν ξεκινήσουν οι πραγματικές μετρήσεις. Αυτό εξομοιώνει ένα “ζεστό” σύστημα που τρέχει. Προκαθορισμένο είναι το 2.
  • time - ο χρόνος σε δευτερόλεπτα για το πόσο κάθε ξεχωριστό σενάριο συγκριτικής αξιολόγησης θα πρέπει να τρέξει και να μετρηθεί. Προκαθορισμένο είναι το 5.
  • inputs - ο χάρτης αλφαριθμητικών που αντιπροσωπεύει το όνομα εισαγωγής σαν το κλειδί και την πραγματική είσοδο σαν τιμή. Προκαθορισμένο το nil. Θα καλύψουμε αυτή την επιλογή σε βάθος στον επόμενο τομέα.
  • parallel - ο αριθμός διεργασιών προς χρήση για την συγκριτική αξιολόγηση των συναρτήσεων σας. Έτσι, αν ορίσετε parallel: 4, τότε 4 διεργασίες θα ξεκινήσουν οι οποίες θα εκτελούν όλες την ίδια συνάρτηση για τον δοθέντα χρόνο. Όταν αυτές τελειώσουν, τότε 4 νέες διεργασίες θα ξεκινήσουν για την επόμενη συνάρτηση. Αυτό σας δίνει περισσότερα δεδομένα στον ίδιο χρόνο, αλλά επίσης βάζει ένα φόρτο στο σύστημά σας ο οποίος παρεμβάλεται στα αποτελέσματα των συγκριτικών αξιολογήσεων. Αυτό μπορεί να είναι χρήσιμο στην εξομοίωση ενός συστήματος σε φόρτο το οποίο μερικές φορές είναι χρήσιμο, αλλά πρέπει να χρησιμοποιείται με προσοχή καθώς μπορεί να επηρεάσει τα αποτελέσματα με απρόβλεπτο τρόπο. Προκαθορισμένο το 1 (το οποίο σημαίνει όχι παράλληλη εκτέλεση).
  • formatters - μια λίστα συναρτήσεων μορφοποιητών που θέλετε να τρέξετε για να εμφανίσετε τα αποτελέσματα των συγκριτικών αξιολογήσεών για τη σουίτα όταν χρησιμοποιείτε την Benchee.run/2. Οι συναρτήσεις πρέπει να αποδέχονται ένα όρισμα (το οποίο είναι η σουίτα συγκριτικών αξιολογήσεων με όλα τα δεδομένα) και τότε να τα χρησιμοποιούν για να παράγουν έξοδο. Προκαθορισμένο είναι ο ενσωματωμένος μορφοποιητής κονσόλας που καλεί την Benchee.Formatters.Console.output/1. Θα το καλύψουμε αυτό σε επόμενο τομέα.
  • print - ένας χάρτης ή μια λίστα λέξεων κλειδί με τις ακόλουθες επιλογές σαν άτομα ως κλειδιά και τιμές είτε true ή false. Αυτό μας επιτρέπει να ελέγχουμε αν η έξοδος ορισμένη από το άτομο θα εκτυπωθεί κατά τη διάρκεια της κανονικής διεργασίας συγκριτικής αξιολόγησης. Όλες οι επιλογές είναι ενεργοποιημένες εξ’ ορισμού (true). Οι επιλογές είναι:
    • benchmarking - εκτυπώση όταν το Benchee ξεκινάει την αξιολόγηση μιας νέας εργασίας.
    • configuration - μια συλλογή ρυθμιζόμενων επιλογών αξιολόγησης συμπεριλαμβανομένου του εκτιμώμενου χρόνου εκτέλεσης εκτυπώνεται πριν ξεκινήσει η αξιολόγηση.
    • fast_warning - εμφανίζονται προειδοποιήσεις αν οι συναρτήσεις εκτελούνται πολύ γρήγορα, το οποίο ενδεχομένως να οδηγεί σε ανακριβή μετρήσεις.
  • console - ένας χάρτης ή μια λίστα λέξεων κλειδί με τις ακόλουθες επιλογές σαν άτομα και τιμές που ποικίλουν. Οι διαθέσιμες τιμές εμφανίζονται κάτω από κάθε επιλογή:
    • comparison - αν η σύγκριση των διαφορετικών εργασιών σύγκρισης (x φορές αργότερο από) εμφανίζεται. Προκαθορισμένο το true, αλλά μπορεί επίσης να οριστεί σε false.
    • unit_scaling - η στρατηγική κλιμάκωσης μονάδας για τις διάρκειες και τις μετρήσεις. Όταν αυξομειώνεται μια τιμή, το Benchee βρίσκει την “καλύτερη” μονάδα μέτρησης (η μεγαλύτερη μονάδα για την οποία το αποτέλεσμα είναι τουλάχιστον 1). Για παράδειγμα, το 1_200_000 αλλάζει σε 1.2 M, ενώ το 800_000 σε 800 K. Η στρατηγική κλιμάκωσης μονάδας καθορίζει πως το Benchee επιλέγει την καλύτερη μονάδα για μια μεγάλη λίστα τιμών, όταν οι επι μέρους τιμές στη λίστα έχουν πολλά διαφορετικές ιδανικές μονάδες μέτρησης. Υπάρχουν τέσσερις στρατηγικές, όλες δοθείσες σαν άτομα, προκαθορισμένο το :best:
      • best - η πιο συχνή ιδανική μονάδα θα χρησιμοποιηθεί. Μια ισοπαλία θα οδηγήσει στην επιλογή της μεγαλύτερης μονάδας.
      • largest - η μεγαλύτερη ιδανική μονάδα θα χρησιμοποιηθεί.
      • smallest - η μικρότερη ιδανική μονάδα θα χρησιμοποιηθεί.
      • none - δεν θα γίνει καμία κλιμάκωση. Οι διάρκειες θα εμφανιστούν σε μικροδευτερόλεπτα, οι ips μετρήσεις θα εμφανιστούν χωρίς μονάδες.

Είσοδοι

Είναι πολύ σημαντικό να κάνουμε συγκριτικές αξιολογήσεις στις συναρτήσεις μας με δεδομένα που αντικατοπρίζουν τα πραγματικά δεδομένα στα οποία θα δράσει η συνάρτηση στην πραγματικότητα. Συχνά η συνάρτηση μπορεί να συμπεριφέρεται διαφορετικά με μικρά σετ δεδομένων από ότι σε μεγάλα σετ δεδομένων! Εδώ δρα η επιλογή ρύθμισης inputs του Benchee. Σας επιτρέπει να ελέγξετε την ίδια συνάρτηση, αλλά με όσες διαφορετικές εισόδους θέλετε, και να δείτε τα αποτελέσματα της συγκριτικής αξιολόγησης για κάθε μία από αυτές τις συναρτήσεις.

Έτσι, ας δούμε το αρχικό μας παράδειγμα ξανά:

list = Enum.to_list(1..10_000)
map_fun = fn i -> [i, i * i] end

Benchee.run(%{
  "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
  "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten() end
})

Σε αυτό το παράδειγμα χρησιμοποιούμε μόνο μια μονή λίστα ακέραιων από το 1 έως το 10,000. Ας το αλλάξουμε ώστε να χρησιμοποιούμε μερικές διαφορετικές εισόδους ώστε να δούμε τι συμβαίνει με τις μικρότερες και μεγαλύτερες λίστες. Έτσι, ανοίξτε το αρχείο, και αλλάξτε το ώστε να μοιάζει ως εξής:

map_fun = fn i -> [i, i * i] end

inputs = %{
  "small list" => Enum.to_list(1..100),
  "medium list" => Enum.to_list(1..10_000),
  "large list" => Enum.to_list(1..1_000_000)
}

Benchee.run(
  %{
    "flat_map" => fn list -> Enum.flat_map(list, map_fun) end,
    "map.flatten" => fn list -> list |> Enum.map(map_fun) |> List.flatten() end
  },
  inputs: inputs
)

Θα παρατηρήσετε δύο διαφορές. Πρώτον, τώρα έχουμε ένα χάρτη inputs που περιέχει την πληροφορία για τις εισαγωγές των συναρτήσεών μας. Αυτό τον χάρτη εισόδου τον περνάμε σαν επιλογή ρυθμίσεων στην Benchee.run/2.

Και από τη στιγμή που οι συναρτήσεις μας χρειάζονται να δεχτούν ένα όρισμα τώρα, πρέπει να αλλάξουμε τις συναρτήσεις συγκριτικών αξιολογήσεων να δέχονται ένα όρισμα, έτσι αντί για:

fn -> Enum.flat_map(list, map_fun) end

τώρα έχουμε:

fn(list) -> Enum.flat_map(list, map_fun) end

Ας το ξανατρέξουμε χρησιμοποιώντας:

$ mix run benchmark.exs

Τώρα θα πρέπει να δείτε έξοδο στην κονσόλα σας ως εξής:

Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-4260U CPU @ 1.40GHz
Number of Available Cores: 4
Available memory: 8.589934592 GB
Elixir 1.5.1
Erlang 20.0
Benchmark suite executing with the following configuration:
warmup: 2.00 s
time: 5.00 s
parallel: 1
inputs: large list, medium list, small list
Estimated total run time: 2.10 min

Benchmarking with input large list:
Benchmarking flat_map...
Benchmarking map.flatten...

Benchmarking with input medium list:
Benchmarking flat_map...
Benchmarking map.flatten...

Benchmarking with input small list:
Benchmarking flat_map...
Benchmarking map.flatten...


##### With input large list #####
Name                  ips        average  deviation         median
flat_map             6.29      158.93 ms    ±19.87%      160.19 ms
map.flatten          4.80      208.20 ms    ±23.89%      200.11 ms

Comparison:
flat_map             6.29
map.flatten          4.80 - 1.31x slower

##### With input medium list #####
Name                  ips        average  deviation         median
flat_map           1.34 K        0.75 ms    ±28.14%        0.65 ms
map.flatten        0.87 K        1.15 ms    ±57.91%        1.04 ms

Comparison:
flat_map           1.34 K
map.flatten        0.87 K - 1.55x slower

##### With input small list #####
Name                  ips        average  deviation         median
flat_map         122.71 K        8.15 μs   ±378.78%        7.00 μs
map.flatten       86.39 K       11.58 μs   ±680.56%       10.00 μs

Comparison:
flat_map         122.71 K
map.flatten       86.39 K - 1.42x slower

Μπορούμε τώρα να δούμε πληροφορίες για τις συγκριτικές αξιολογήσεις μας, συγκεντρωμένες κατά είσοδο. Αυτό το απλό παράδειγμα δεν μας παρέχει καμμιά εξαιρετική επίγνωση, αλλά θα εκπλαγείτε στο κατά πόσο διαφέρουν οι επιδόσεις ανάλογα με το μέγεθος εισαγωγής!

Μορφοποιητές

Η έξοδος κονσόλας που είδαμε είναι πολύ χρήσιμη σαν αρχή για τις μετρήσεις των εκτελέσεων των συναρτήσεών σας, αλλά δεν είναι η μόνη σας επιλογή! Σε αυτό τον τομέα θα δούμε σύντομα τους τρεις άλλους διαθέσιμους μορφοποιητές, και επίσης θα κάνουμε μια αρχή στο πως μπορείτε να γράψετε τον δικό σας μορφοποιητή αν θέλετε.

Άλλοι μορφοποιητές

Το Benchee έχει ένα μορφοποιητή κονσόλας προκαθορισμένο, ο οποίος είναι αυτός που έχουμε δει ήδη, αλλά υπάρχουν άλλοι τρεις επίσημα υποστηριζόμενοι - οι benchee_csv, benchee_json και benchee_html. Κάθε ένας από αυτούς κάνει ακριβώς αυτό που θα περιμένατε, το οποίο είναι να γράφει τα αποτελέσματα στο αντίστοιχο τύπο αρχείου ώστε να μπορείτε να δουλέψετε με τα αποτελέσματα περαιτέρω σε οποιοδήποτε φορμάτ θέλετε.

Κάθε ένας από αυτούς τους μορφοποιητές είναι ένα ξεχωριστό πακέτο, έτσι για να τους χρησιμοποιήσετε πρέπει να τους προσθέσετε σαν εξαρτήσεις στο αρχείο mix.exs σας ως εξής:

defp deps do
  [
    {:benchee_csv, "~> 0.6", only: :dev},
    {:benchee_json, "~> 0.3", only: :dev},
    {:benchee_html, "~> 0.3", only: :dev}
  ]
end

Παρόλο που οι benchee_json και benchee_csv είναι πολύ απλές, η benchee_html είναι στην πραγματικότητα πολύ πλήρης σε χαρακτηριστικά! Μπορεί να σας βοηθήσει να δημιουργήσετε όμορφα γραφήματα και διαγράμματα από τα αποτελέματά σας πολύ εύκολα και ακόμα και να τα εξάγετε σαν εικόνες PNG. Όλοι οι τρεις μορφοποιητές είναι πολύ καλά τεκμηριωμένοι στις αντίστοιχες σελίδες τους στο GitHub, έτσι δεν θα καλύψουμε τις λεπτομέρειες τους εδώ.

Ειδικοί Μορφοποιητές

Αν οι τέσσερις προσφερόμενοι μορφοποιητές δεν είναι αρκετοί για εσάς, μπορείτε επίσης να γράψετε το δικό σας μορφοποιητή, το οποίο είναι σχετικά εύκολο. Πρέπει να γράψετε μια συνάρτηση που δέχεται μια δομή %Benchee.Suite{}, και από αυτή μπορείτε να τραβήξετε ότι πληροφορία θέλετε. Πληροφορίες για το τι περιλαμβάνεται σε αυτή τη δομή μπορούν να βρεθούν στο GitHub ή στα HexDocs. Ο κώδικας είναι πολύ καλά τεκμηριωμένος και εύκολα αναγνώσιμος αν θα θέλατε να δείτε του τι είδους πληροφορίες μπορούν να είναι διαθέσιμες για την εγγραφή των δικών σας μορφοποιητών.

Για τώρα, θα δείξω ένα γρήγορο παράδειγμα του πως μπορεί να δείχνει ένας παραμετροποιημένος μορφοποιητής σαν παράδειγμα του πόσο εύκολο είναι. Ας πούμε ότι θέλουμε απλά έναν απλό μορφοποιητή που τυπώνει μόνο τον μέσο χρόνο εκτέλεσης για κάθε σενάριο - θα δείχνει κάπως έτσι:

defmodule Custom.Formatter do
  def output(suite) do
    suite
    |> format
    |> IO.write()

    suite
  end

  defp format(suite) do
    Enum.map_join(suite.scenarios, "\n", fn scenario ->
      "Average for #{scenario.job_name}: #{scenario.run_time_statistics.average}"
    end)
  end
end

Και τότε θα μπορούμενα τρέξουμε τη συγκριτική αξιολόγησή μας ως εξής:

list = Enum.to_list(1..10_000)
map_fun = fn i -> [i, i * i] end

Benchee.run(
  %{
    "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
    "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten() end
  },
  formatters: [&Custom.Formatter.output/1]
)

Και όταν τρέξουμε τώρα με τον τρέχων μορφοποιητή, θα πρέπει να δούμε:

Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-4260U CPU @ 1.40GHz
Number of Available Cores: 4
Available memory: 8.589934592 GB
Elixir 1.5.1
Erlang 20.0
Benchmark suite executing with the following configuration:
warmup: 2.00 s
time: 5.00 s
parallel: 1
inputs: none specified
Estimated total run time: 14.00 s


Benchmarking flat_map...
Benchmarking map.flatten...
Average for flat_map: 851.8840109326956
Average for map.flatten: 1659.3854339873628