Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Big-O表記法のRubyistsガイド

私はコンピュータサイエンスの学位を持っていません。私たちの多くのRubyistはそうしません。そのため、長い間、big-O表記について学ぶことを避けていました。それは少し高等数学に似すぎています。つまり、O(N^2) 。来て。

代わりに、経験則を学びました:

  • ハッシュで特定のアイテムを見つけるのは、配列よりも高速です
  • ネストされたループを避ける
  • ビューでリストを生成するときに、偶発的なDBクエリに注意してください

これらのルールは優れていますが、なぜを理解していない限り それらは機能し、間違いを犯し、不可解なパフォーマンスの問題を抱えていることに気付くでしょう。

なぜそれが重要なのですか?

Big-O表記は、コードのパフォーマンスが処理するデータの量にどのように依存するかを説明するための優れた方法です。

パフォーマンスとは、速度またはRAMの使用量の2つのうちの1つを意味します。コンピュータサイエンスのクラスでは、これらを「時間計算量」および「空間計算量」と呼びます。 Big-O表記は両方に使用されますが、ここでは速度に焦点を当てます。これは、より一般的な使用法のように思われるためです。

100アイテムの配列の処理は、10アイテムの配列よりも遅くなることが予想されます。しかし、いくらですか? 10倍、100倍? 1000倍?

小さなデータセットでは大したことではありませんが、データベースの各行でアプリが指数関数的に遅くなると、すぐに問題になります。

詳細に入る前に、データのスケーリングに応じてどのように感じられるかを示す絵文字付きの一般的なビッグOを示す簡単なグラフを作成しました。

Bi​​g O ランク 意味
O(1) 😎 速度はデータセットのサイズに依存しません
O(log n) 😁 データの10倍は、2倍の時間がかかることを意味します
O(n) 😕 データの10倍は、10倍の時間がかかることを意味します
O(n log n) 😖 データの10倍は、約20倍の時間がかかることを意味します
O(n ^ 2) 😫 データの10倍の時間が100倍長くなります
O(2 ^ n) 😱 ダイリチウム結晶が壊れています!

つまり、誰かがArray#bsearchと言ったとき Array#findよりも優れています O(log n)だからです vs O(n) 😁と😕を比較して、それらが何かにある可能性があることを確認できます。

もう少し堅牢なものについては、Big-Oチートシートを確認してください

表記の解読

表記がどのように機能するかを理解している限り、さまざまなBig-O値をすべて覚える必要はありません。

たとえば、恐ろしい恐ろしいO(2^n) 。それをRubyで表現すると、次のようになります。

# O(2^n) translated to Ruby
def o(n)
  2 ** n  # This is ruby for 2^n
end

まだ明らかではありませんか?メソッドと引数の名前をよりユーザーフレンドリーなものに変更した場合はどうでしょうか?

# O(2^n) translated to prettier Ruby
def execution_time(size_of_dataset)
  2 ** size_of_dataset
end

あなたはそれらすべてのためにこれを行うことができます:

# O(1)
def o1_execution_time(size_of_dataset)
  1
end

# O(n)
def on_execution_time(size_of_dataset)
  size_of_dataset
end

# O(n^2)
def on2_execution_time(size_of_dataset)
  size_of_dataset * size_of_dataset
end

...etc

表記法がどのように機能するかがわかったので、いくつかの典型的なルビーコードを見て、それがどのように関連するかを見てみましょう。

O(1)

何かがO(1)であると言うとき これは、その速度がデータセットのサイズに依存しないことを意味します。

たとえば、ハッシュルックアップ時間はハッシュサイズに依存しません:

# These should all take the same amount of time
hash_with_100_items[:a]
hash_with_1000_items[:a]
hash_with_10000_items[:a]

これが、大きなデータセットの場合、ハッシュは配列よりも高速であると言う傾向がある理由です。

O(n)

対照的に、Array#find O(n)です 。つまり、Array#find 配列内のアイテムの数に直線的に依存します。 100個のアイテムを含む配列は、1個のアイテムを含む配列よりも検索に100倍の時間がかかります

配列を反復処理する多くのコードは、O(n)に従います。 パターン。

(0..9).each do |i|
  puts i
end

# This example is 1/2 the speed of the previous because it contains 2x the items. 
(0..19).each do |i|
  puts i
end

O(n ^ 2)

O(n^2)に適合するコード プロファイルにはネストされたループが含まれる傾向があります。あなたがそれについて考えるならば、それは理にかなっています。 1つのループでO(n)が得られます 、2番目のネストされたループはO(n^2)を提供します 。なんらかの理由で、5レベルのネストされたループがあった場合、それはO(n^5)になります。 。

data = (0..100)
data.each do |d1|
  data.each do |d2|
    puts "#{ d1 }, #{ d2 }"
  end
end

O(n log n)

O(n log n) コードは、多くの場合、誰かが他の方法でO(n^2)する作業量を減らすための賢い方法を見つけた結果です。 アルゴリズムで十分です。

コードの一部を見て、それがO(n log n)であると言うことはできません。 。これは、より高度な数学が登場する場所であり、私がお辞儀をする場所でもあります。

ただし、O(n log n)について知っておくことが重要です。 多くの一般的な検索アルゴリズムについて説明しているためです。 RubyのArray#sort 由緒あるクイックソートアルゴリズムを使用します。これは平均してO(n log n)です。 最悪の場合はO(n^2)

クイックソートに慣れていない場合は、この優れたデモを確認してください。

すべてをまとめる:データベース

新しいWebアプリケーションで最も一般的な問題の1つは、開発者のコ​​ンピューターでは高速ですが、本番環境ではますます遅くなることです。

これは、データベース内のレコードの量が時間の経過とともに増加するために発生しますが、コードはDBに適切にスケーリングされない操作を実行するように要求しています。すなわち。 O(n) またはさらに悪い。

たとえば、postgresでは、カウントクエリは常にO(n)であることをご存知ですか。 ?

# This makes the DB iterate over every row of Users
# ...unless you're using a Rails counter cache. 
Users.count

これは、postgresのexplainを使用して確認できます。 指図。以下では、これを使用して、カウントクエリのクエリプランを取得します。ご覧のとおり、テーブル内の104,791行すべてに対してシーケンシャルスキャン(ループを意味する)を実行することを計画しています。

# explain select count(*) from users;
                           QUERY PLAN
-----------------------------------------------------------------
 Aggregate  (cost=6920.89..6920.90 rows=1 width=0)
   ->  Seq Scan on users  (cost=0.00..6660.71 rows=104701 width=0)
(2 rows)

多くの一般的なレールイディオムは、データベースを特に最適化してそれらを防止しない限り、意図しないシーケンシャルスキャンをトリガーする可能性があります。

# This asks the DB to sort the entire `products` table
Products.order("price desc").limit(1)

# If `hobby` isn't indexed, the DB loops through each row of Users to find it. 
User.where(hobby: "fishing")

explainを使用できます それも確認するコマンド。ここでは、テーブル全体でソート(おそらくクイックソート)を実行していることがわかります。メモリの制約がある場合は、パフォーマンス特性が異なる別の並べ替えアルゴリズムを選択した可能性があります。

# explain select * from users order by nickname desc limit 1;
                               QUERY PLAN
-------------------------------------------------------------------------
 Limit  (cost=7190.07..7190.07 rows=1 width=812)
   ->  Sort  (cost=7190.07..7405.24 rows=104701 width=812)
         Sort Key: nickname
         ->  Seq Scan on users  (cost=0.00..6606.71 rows=104701 width=812)

もちろん、これらすべての問題に対する答えは索引付けです。ハッシュルックアップを使用する場合、データベースにインデックスを使用するように指示するのは、Rubyの場合と少し似ていますO(1) 配列の代わりにO(n)を見つけます 。

それだけです、皆さん

これがBig-O表記法の有用な紹介であり、ルビー開発者としてのあなたにどのような影響を与えるかを願っています。ご不明な点がございましたら、@StarrHorneまでpingしてください。


  1. RubyでBig-O表記を調べる

    「そのためのBig-O表記は何ですか?」という質問を聞くことほど私を怖がらせなかった時期がありました。学校での話題を思い出しましたが、数学と関係があったので(これは私の最強の科目ではありませんでした)、それを黒く塗りつぶしました。 しかし、私のキャリアが進むにつれて、私は自分自身に気づきました: パフォーマンスチャートを見る 遅いクエリをデバッグしようとしています 負荷が増加した場合にコードがどのように保持されるかを検討したかどうかを尋ねられます Big-Oを学ぶために戻って(わかりますか?)と決めたとき、私はその高レベルの単純さに驚いていました。この記事で学んだことを共有して、エンジ

  2. Rubyでの関数型プログラミング(完全ガイド)

    関数型プログラミングについて聞いたばかりで、いくつか質問があるかもしれません。 いいね… 関数型プログラミングとは正確には何ですか? オブジェクト指向プログラミングと比較してどうですか? Rubyで関数型プログラミングを使用する必要がありますか? これらの質問に答えて、これがどのように機能するかをよりよく理解できるようにします。 関数型プログラミングとは これは単なる流行や派手な言葉ではなく、長い間存在していた実際のプログラミングパラダイムですが、最近人気を取り戻しています。 そして、このパラダイムの背後にある基本的な考え方は、あなたが思っているよりも理解しやすいです。 関数型