Do you want to pick up from where you left of?
Take me there

Benchee

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

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

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

Χρήση

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

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

Then we call:

$ 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: Linux
CPU Information: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
Number of Available Cores: 8
Available memory: 15.61 GB
Elixir 1.8.1
Erlang 21.3.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 14 s

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

Name                  ips        average  deviation         median         99th %
flat_map           2.40 K      416.00 μs    ±12.88%      405.67 μs      718.61 μs
map.flatten        1.24 K      806.20 μs    ±20.65%      752.52 μs     1186.28 μs

Comparison:
flat_map           2.40 K
map.flatten        1.24 K - 1.94x slower +390.20 μs

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

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

Υπάρχουν επίσης άλλα διαθέσιμα στατιστικά, αλλά αυτές οι τέσσερις είναι συχνά οι πιό χρήσιμες και συχνά χρησιμοποιούμενες για συγκριτικές αξιολογήσεις, γι’ αυτό και εμφανίζονται στον προκαθορισμένο μορφοποιητή. Για να μάθετε περισσότερα για τις άλλες διαθέσιμες μετρήσεις, ελέγξτε την τεκμηρίωση στα 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],
  print: [
    benchmarking: true,
    configuration: true,
    fast_warning: true
  ],
  console: [
    comparison: true,
    unit_scaling: :best
  ]
)

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

Είσοδοι

Είναι πολύ σημαντικό να κάνουμε συγκριτικές αξιολογήσεις στις συναρτήσεις μας με δεδομένα που αντικατοπρίζουν τα πραγματικά δεδομένα στα οποία θα δράσει η συνάρτηση στην πραγματικότητα. Συχνά η συνάρτηση μπορεί να συμπεριφέρεται διαφορετικά με μικρά σετ δεδομένων από ότι σε μεγάλα σετ δεδομένων! Εδώ δρα η επιλογή ρύθμισης 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: Linux
CPU Information: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
Number of Available Cores: 8
Available memory: 15.61 GB
Elixir 1.8.1
Erlang 21.3.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
parallel: 1
inputs: large list, medium list, small list
Estimated total run time: 42 s

Benchmarking flat_map with input large list...
Benchmarking flat_map with input medium list...
Benchmarking flat_map with input small list...
Benchmarking map.flatten with input large list...
Benchmarking map.flatten with input medium list...
Benchmarking map.flatten with input small list...

##### With input large list #####
Name                  ips        average  deviation         median         99th %
flat_map            13.20       75.78 ms    ±25.15%       71.89 ms      113.61 ms
map.flatten         10.48       95.44 ms    ±19.26%       96.79 ms      134.43 ms

Comparison:
flat_map            13.20
map.flatten         10.48 - 1.26x slower +19.67 ms

##### With input medium list #####
Name                  ips        average  deviation         median         99th %
flat_map           2.66 K      376.04 μs    ±23.72%      347.29 μs      678.17 μs
map.flatten        1.75 K      573.01 μs    ±27.12%      512.48 μs     1076.27 μs

Comparison:
flat_map           2.66 K
map.flatten        1.75 K - 1.52x slower +196.98 μs

##### With input small list #####
Name                  ips        average  deviation         median         99th %
flat_map         266.52 K        3.75 μs   ±254.26%        3.47 μs        7.29 μs
map.flatten      178.18 K        5.61 μs   ±196.80%        5.00 μs       10.87 μs

Comparison:
flat_map         266.52 K
map.flatten      178.18 K - 1.50x slower +1.86 μs

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

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

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

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

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

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

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

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

benchee_html graph export sample

Όλοι οι τρεις μορφοποιητές είναι πολύ καλά τεκμηριωμένοι στις αντίστοιχες σελίδες τους στο 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_data.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: Linux
CPU Information: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
Number of Available Cores: 8
Available memory: 15.61 GB
Elixir 1.8.1
Erlang 21.3.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 14 s

Benchmarking flat_map...
Benchmarking map.flatten...
Average for flat_map: 419433.3593474056
Average for map.flatten: 788524.9366408596

Μνήμη

Σχεδόν φτάσαμε στο τέλος, αλλά καθ’ όλη αυτή τη διάρκεια, δεν σας δείξαμε μια από τις πιο αξιόλογες ιδιότητες του Benchee: τις μετρήσεις μνήμης!

Το Benchee είναι σε θέση να μετρήσει την κατανάλωση μνήμης, αλλά περιορίζεται στην διεργασία για την οποία εκτελείται η συγκριτική σας αξιολόγηση. Προς το παρόν δεν μπορεί να ιχνηλατήσει την κατανάλωση μνήμης σε άλλες διεργασίες (όπως οι λίστες εργατών).

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

Πως το χρησιμοποιείτε; Απλά χρησιμοποιείτε την επιλογή :memory_time!

Operating System: Linux
CPU Information: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
Number of Available Cores: 8
Available memory: 15.61 GB
Elixir 1.8.1
Erlang 21.3.2

Benchmark suite executing with the following configuration:
warmup: 0 ns
time: 0 ns
memory time: 1 s
parallel: 1
inputs: none specified
Estimated total run time: 2 s

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

Memory usage statistics:

Name           Memory usage
flat_map          624.97 KB
map.flatten       781.25 KB - 1.25x memory usage +156.28 KB

**All measurements for memory usage were the same**

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

Έπιασες λάθος ή θέλεις να συνεισφέρεις στο μάθημα; Επεξεργαστείτε αυτό το μάθημα στο GitHub!