コードを書くときに意識したい、少し低いレイヤの基礎知識
こんばんは、最近勉強が停滞気味のwakasaです。
停滞気味で何かにまとめないと身につかない気がするので、今回は少し低いレイヤの超基礎の知識をまとめていきたいと思います。
対象としては、
「Railsある程度書けるようになってきたけど、メモリとかCPUの話とどう関係があるかわからん。」
「ActiveRecordで処理を書いていて、先輩にキャッシュ云々言われたけどどういうことかよくわからない。」
「SQL発行多いと言われたけど、何が悪いの?」
という半年前の自分のようなプログラミング歴半年程度の人を対象に書いていきます。
私も低レイヤはまだまだ勉強中の身なので、もし認識違いの部分や理解が誤っている点があればご指摘頂けると幸いです。
プログラムの実行に必要なもの
まずは誰でも知っているプログラムの実行に必要な構成要素からです。
PCの主要な要素は下記の三つがあります。
①CPU
②メモリ
③ディスク(HDD/SSDなど)
他にもディスプレイやキーボードとか色々とあるだろう!と怒られそうですが、とりあえずプログラムを実行という観点で見れば、下記三つがあれば実行できます。
この三つは流石にどなたでもご存知だと思いますが、それでは、それぞれはどのような役割があり、どのように実行されるのでしょうか?
とても基本的なことなのですが、半年前の自分はそれすら分かっていませんでした…
ざっくりした実行の順序
プログラムの実行はまずディスクから必要なプログラムのソースやデータがメモリ上に読み込まれます。
そこからさらにCPUに読み込まれ(正確にはCPUのレジスタ)各種命令が実行されます。その結果が、またメモリに書き込まれ、メモリからディスクに書き込まれたりします。
ざっくり概念的に書くと下記のような感じになります。
実行速度に影響を与えるもの
ここでRailsを書く上で大きなポイントが一つあります。それは各種アクセスにかかるスピードです。各種アクセスというのは、
CPU ↔︎ メモリ
メモリ ↔︎ ディスク
のそれぞれの間のアクセス速度の違いです。業務でコードを書く上では、最低限アクセス速度の違いは最低限、理解する必要があります。
(自分がわかってなくて苦しんだ)
どのくらい違うのか、こちらの記事によると、メモリにアクセスする場合と、ディスクにアクセスする場合では、最低でも100倍、最大で1万倍近い差があることがわかります。
つまりメモリにアクセスする場合と、ディスクにアクセスする場合ではとてつもない差があることがわかります。
また実際にはCPUとメモリの間にはL1 ~ L3のキャッシュメモリという層があり、その層はメモリより高速に動作し、一度読み出された値を保持してくれたりします。
そのため上記の差はさらに大きくなります。
参考となる図:マルチコアCPU上の並列化手法、その並列性能と問題点
そのためRailsで処理を書くときもこれはディスクアクセスを伴う処理なのか、メモリにアクセスしているのかを意識する必要があります。
特にモデル周りでDBアクセスを伴う処理を書くときはここを気をつけないと、思いがけずかなり時間を取る処理を知らぬ間に書いてしまうことがしばしばあります。
実際にRailsで見てみる(書き込み編)
それでは実際にRailsの実例で見ていきましょう。まずは書き込みです。
こちらは初心者の方でも使ったことがあると思いますが、ActiveRecordのbuildとsaveとcreateの違いです。
例えばUserモデルがあったときに下記のようにすることもあると思います。
user = User.build(name: 'hoge', age: 18) user.save!
buildだと保存はされず、saveを使うとDBに保存されますよね?そして、createはbuildとsaveを同時に行う、といったように習ったはずです。
それを先の概念で見ると、まずbuildの場合はメモリ上のインスタンスの値が変更されることになります。そしてsaveを行うことによって、DBにSQLが発行されディスクに保存されます。
メモリ上の値はディスクに保存しないと消えてしまうので、もしsaveしないとそのうち消えることになります。そのため消えても良いデータなのか、DBに保存すべきデータなのかはきちんと分けて考えないといけないですし、DBのIndex作成や大量にデータを作成する時は、それなりのディスクへの書き込みアクセスが走ることになるので、リソースに余裕のないプロダクトの場合、CPUやディスクアクセスへの負荷を考える必要があります。
実際にRailsで見てみる(読み込み編)
読み込みの場合は、メモリにデータが乗っているか否かやSQL発行の有無、目的のデータが取れているか、さらに言うと、どのデータをメモリに乗っけて、Rubyレイヤ、SQLレイヤのどちらが処理を担うかを考えなくてはいけません。
そのためにもどの処理はSQLを発行し、どの処理はSQLを発行しないのかをざっくりとでも理解することが重要になります。なぜかというとDBへのアクセスはディスクアクセスになるので、非常に時間を取られるからです。
そう考えるとN+1が何故ダメかわかると思います。毎回ディスクアクセスが走ると待ちが発生しますし、時間がかかりますよね。そのため基本的にはN+1ではなく、一気にデータをメモリ上に取ってきて、Rubyレイヤで順次処理する方が良いというのが理解できると思います。(レコード数や構造、インデックスややりたいことなど条件にもよりますが)
例えば、ブログサイトを作っていて、ブログに紐付くユーザーを取ってくる時、
blogs = Blog.where(status: :published) blogs.each do |blog| puts blog.user.name end
とするより、
blogs = Blog.where(status: :published).includes(:user) blogs.each do |blog| puts blog.user.name end
とした方が、includesはデータをキャッシュして読み込んでくれるので、処理が早いということになります。
そうしたディスクアクセスに対するコスト意識が少しずつ必要になってきますし、DBやSQLに関する知識も求められるようになります。
(まさに自分が最近DB系の知識不足を痛感しています…)
終わりに
自分もまだまだ勉強中の身ですが、初心者でも知っておいた方が良さそうな、基礎的な部分をまとめてみました。
参考になれば幸いです。