Stackoverflow.comの高速化

約3週間前、私はこのトピックのハブで、人気サイトStackoverflowの主要な開発者の1人によるDapper - ORMについて読みました。 このスーパーヒーローの名前はサムサフラン (以下、単にサムと呼ぶ )です。 さらに、Stackoverflowアーキテクチャに関するこのトピックが登場する前に、 Linq-to-Sqlを使用することが知られていました。 これが、他の開発者と同様に、Dapperのソースコードの研究を始めた主な理由です。 それが判明したように、それは少し、またはむしろ1つのファイルです。 注意深く調べてみて、もっと速くできるのではないかと思いました。 Samのコードを高速化するのは簡単ではありませんでした。 次に、他の開発者向けのヒントの形で、マイクロ最適化について説明します。 しかし、最初に、いくつかの開発者に警告したいと思います。 説明した最適化により、Dapperは5%加速しました。これはStackoverflowなどのプロジェクトにとって不可欠ですが、プロジェクトにとっては重要ではない場合があります。 したがって、 プロファイリング結果に基づいてマクロ最適化のオプション(トピックの最後の例)を常に考慮し、特別な場合にのみマイクロ最適化に頼ってください





常に最小限の契約を使用してください。


厳密に言えば、これはコードをより良くし、変更に対する抵抗力を高めるだけですが、実行速度を上げることはありません。 正しい契約を決定するのは簡単な場合もあればそうでない場合もあります。 たとえば、IListを返す意味がない場合、コードの残りの部分がコレクションに対して単純な反復を実行する場合。 IEnumerableを返すだけです。 このインターフェイスを選択すると、サムは次のバージョンでreturn yield構文を使用できます。

public static IEnumerable<T> ExecuteMapperQuery<T>(this IDbConnection con, string sql, object param = null, SqlTransaction transaction = null) { using (var reader = GetReader(con, transaction, sql, GetParamInfo(param))) { var identity = new Identity(sql, con.ConnectionString, typeof(T)); var deserializer = GetDeserializer<T>(identity, reader); while (reader.Read()) { yield return deserializer(reader); } } }
      
      





自明でない選択として、 IDataReaderインターフェースについて言及します。 Samは、このインターフェイスをサポートするオブジェクトにFieldCountプロパティをよく使用します。 ただし、インターフェイスの完全な階層を注意深く調べると、FieldCountが実際にIDataRecordインターフェイスに属していることがわかります。



契約を削除することを検討してください


このアドバイスは前のアドバイスの結果であるため、詳細については説明しません。 契約が非常に小さいため、安全に削除できる場合があります。 次の例では、 IDbConnectionの代わりに単に文字列を渡すことができます。

 private class Identity : IEquatable<Identity> { private readonly string connectionString; internal Identity(string sql, IDbConnection cnn, Type type) { // ... this.connectionString = cnn.ConnectionString; // ... } }
      
      





予測することを学ぶ


少し奇妙に聞こえますよね? ただし、ここでは、アルゴリズムの論理的な動作を決定することについて話しています。 予測は正確かつ不正確です。 最初に不正確なことを検討してください。 ここでは、どのように、そして何が起こるかを確実に言うことはできません。 たとえば、Dapperは既知のタイプの辞書を作成します。 一定の量に達すると、ユーザーが新しい要素を追加した場合に辞書がサイズを大きくするのに時間がかかることがわかっています。 この場合、どのような予測ができますか? 簡単なことは、すべてのタイプを再カウントし、すぐに必要なメモリ量を辞書に伝えることです。 私は35を得た:

 public static class SqlMapper { static readonly Dictionary<Type, DbType> typeMap; static SqlMapper() { // ... TypeMap = new Dictionary<Type, DbType>(35); TypeMap[typeof(byte)] = DbType.Byte; // ... } }
      
      





そして、不正確さは何ですか? この数は変更に大きく依存しているため、新しいタイプを追加すると無効になります。 もちろん、悪いことは何も起こりませんが、コードはより信頼性が低くなり、予測は間違っています。

正確な予測は非常に優れており、可能な限り使用する必要があります。 このような予測の顕著な例は、要素の数が正確にわかっている場合にリストを配列に置き換えることです。 主な理由は、辞書の場合と同じです。 つまり、メモリの再割り当て。 もう1つの重要な理由は、配列内のインデックスによる割り当て操作が、リストでAddメソッドを呼び出すよりもはるかに速いことです。 これは、コード生成の例で明らかに見られます。

 private static Func<object, List<ParamInfo>> CreateParamInfoGenerator(Type type) { DynamicMethod dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), typeof(List<ParamInfo>), new Type[] { typeof(object) }, true); var il = dm.GetILGenerator(); // ... il.Emit(OpCodes.Newobj, typeof(List<ParamInfo>).GetConstructor(Type.EmptyTypes)); //   foreach (var prop in type.GetProperties().OrderBy(p => p.Name)) { // ... il.Emit(OpCodes.Callvirt, typeof(List<ParamInfo>).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance)); //   Add } // ... }
      
      





そして、配列の場合:

 private static Func<object, IEnumerable<ParamInfo>> CreateParamInfoGenerator(Type type) { var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid(), typeof(IEnumerable<ParamInfo>), new[] { typeof(object) }, true); var il = dm.GetILGenerator(); // ... var properties = type.GetProperties(); il.Emit(OpCodes.Ldc_I4_S, properties.Length); il.Emit(OpCodes.Newarr, typeof(ParamInfo)); //   PropertyInfo prop; for (var i = 0; i < properties.Length; i++) { prop = properties[i]; // ... EmitInt32(il, i); //      // ... il.Emit(OpCodes.Stelem_Ref); //     } // ... }
      
      





反射の使用を減らす


アドバイスは明白であり、反射は非常に遅いメカニズムであることを誰もが知っています。 サムもこれを知っており、彼はコード生成を使用してオブジェクトのメソッドとプロパティの処理を高速化します(この場合、手動生成に反対し、式ツリーは価値のある代替と考えています)。 リフレクションのコストに対処する一般的に受け入れられている2番目の方法、キャッシングがあります。 Dapperでは、新しいクラスごとにデシリアライザーが作成されます。 行を見つけることができる作成コードで:

 var getItem = typeof(IDataRecord).GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(p => p.GetIndexParameters().Any() && p.GetIndexParameters()[0].ParameterType == typeof(int)) .Select(p => p.GetGetMethod()).First();
      
      





明らかに、この情報はキャッシュできます。 getItemをクラスレベルにし、静的コンストラクターで初期化します。



ダブルチェック回路


ほとんどの場合、プログラマーは意識せずにクロージャーを作成し(C#で以前に会ったことがない人のために、このリンクに従うことをお勧めします)、予期しないエラーでそれらを支払います(サムもキャッチされました!)。 ただし、クロージャを使用して加速することができます。 例:

 private static object GetDynamicDeserializer(IDataReader reader) { List<string> colNames = new List<string>(); for (int i = 0; i < reader.FieldCount; i++) { colNames.Add(reader.GetName(i)); } Func<IDataReader, ExpandoObject> rval = r => { IDictionary<string, object> row = new ExpandoObject(); int i = 0; foreach (var colName in colNames) { var tmp = r.GetValue(i); row[colName] = tmp == DBNull.Value ? null : tmp; i++; } return (ExpandoObject)row; }; return rval; }
      
      





ラムダ式でわかるように、ローカル変数colNamesにクロージャーが作成され、列名の受け取りが高速化されます。 理論的には、これによりパフォーマンスが向上します。 結局、IDataReaderのすべてのレコードを反復処理しても、列の名前は変わりません。 残念ながら、たとえば、 SqlDataReader開発者もこれについて考え、クラス内の同様の配列に列名を保存したため、次のコードは以前のものと似ていますが、クロージャはありません。

 private static Func<IDataRecord, ExpandoObject> GetDynamicDeserializer() { return r => { IDictionary<string, object> row = new ExpandoObject(); for (var i = 0; i < r.FieldCount; i++) { var tmp = r.GetValue(i); row[r.GetName(i)] = tmp == DBNull.Value ? null : tmp; } return (ExpandoObject)row; }; }
      
      





文字列の複数の連結操作を避ける


はい。すべての.Net開発者は、 StringBuilderを使用して複数行の文字列を作成する必要があることを知っています。 しかし、これは何行ですか? 2行または3行の場合、StringBuilderの作成は無駄です。 例:

 private static IDbCommand SetupCommand(IDbConnection cnn, IDbTransaction tranaction, string sql, List<ParamInfo> paramInfo) { // ... cmd.CommandText = cmd.CommandText.Replace("@" + info.Name, "(" + string.Join( ",", Enumerable.Range(1, count).Select(i => "@" + info.Name + i) ) + ")"); // ... }
      
      





「@」+ info.Name + iの形式の文字列に興味があります。 これは、 IDbCommandパラメーターの名前です。 そして、メモリ内のそのような名前ごとに、3行が作成されます。 パラメータがテキストと呼ばれた場合、行は次のようになります。

 @ @text @text1
      
      





原則として、少しですが、5つのパラメーターには15行があります。 StringBuilderの時間ですか? いいえ、おそらくそうではありません。 コードの残りの部分を分析すると、「@」+ info.Name構造が非常に頻繁に使用されていることがわかります。そのため、変数infoNameに置き換えてください。 そのため、行を保存し、さらにプロパティにアクセスするときに保存します。 その結果、5つのパラメーターの場合、6行のみ(infoNameに1行、各連結操作に1行)。



変数の使用場所に可能な限り近い変数を定義したり、その逆にループ外に変数を移動したり、if-elseステートメントの不要な分岐を破棄したり、使用場所に短いメソッドを埋め込んだりといった些細なことについて話を続けることができます。 しかし、マクロの最適化についてお話ししたいです。 Samは現在、IDbCommandへのパラメーターの追加を高速化するために取り組んでいます。 外部のオブザーバーとして、コマンドの再利用とその準備に注意することをお勧めします(これはSqlCommandPrepareメソッドでうまく機能します)。



Dapperが作業からリリースに移行するときに、別のレビューを行うこともありますが、今のところは、このプロジェクトを注意深く監視し、Samの幸運を祈ります。



PS:作者は市民としての義務を果たそうとしてGitHubにプルリクエストを送信しましたが、残念ながら、作者がHabrに関するトピックを書いている間にサムはDapperを開発し、リクエストは無関係になりました。 しかし、作者はサムに手紙を書き、Dapperのリリースにおけるすべての要望を考慮することを約束しました。



All Articles