12

外出先で文字列をさまざまなデータ型に変換するための最速の(一般的なアプローチ)を探しています。

何かによって生成された大きなテキストデータファイルを解析しています(ファイルのサイズは数メガバイトです)。この特定の関数は、テキストファイルの行を読み取り、各行を区切り文字に基づいて列に解析し、解析された値を.NETDataTableに配置します。これは後でデータベースに挿入されます。FARによる私のボトルネックは、文字列変換(ConvertおよびTypeConverter)です。

ファイルにどのタイプが含まれるかわからないため、動的な方法(つまり、「Convert.ToInt32」などから離れる)を使用する必要があります。タイプは、実行時の以前の構成によって決定されます。

これまで私は次のことを試しましたが、どちらもファイルの解析に数分かかります。この1行をコメントアウトすると、数百ミリ秒で実行されることに注意してください。

row[i] = Convert.ChangeType(columnString, dataType);

TypeConverter typeConverter = TypeDescriptor.GetConverter(type);
row[i] = typeConverter.ConvertFromString(null, cultureInfo, columnString);

誰かがこのような一般的なより速い方法を知っているなら、私はそれについて知りたいです。または、私のアプローチ全体が何らかの理由でうまくいかない場合は、提案を受け入れます。ただし、ハードコードされた型を使用する一般的でないアプローチを指摘しないでください。それはここでは単にオプションではありません。

更新-パフォーマンステストを改善するためのマルチスレッド

パフォーマンスを向上させるために、解析タスクを複数のスレッドに分割することを検討しました。速度は多少向上しましたが、思ったほどではありませんでした。しかし、これが興味のある人のための私の結果です。

システム:

Intelキセノン3.3GHzクアッドコアE3-1245

メモリ:12.0 GB

Windows 7 Enterprise x64

テスト:

テスト機能は次のとおりです。

(1)文字列の配列を受け取ります。(2)文字列を区切り文字で分割します。(3)文字列をデータ型に解析し、行に格納します。(4)データテーブルに行を追加します。(5)終了するまで(2)-(4)を繰り返します。

テストには10​​00個の文字列が含まれ、各文字列は16列に解析されるため、合計で16000個の文字列変換が行われます。シングルスレッド、4スレッド(クアッドコアのため)、および8スレッド(ハイパースレッディングのため)をテストしました。ここではデータを処理しているだけなので、これよりも多くのスレッドを追加しても効果があるとは思えません。したがって、シングルスレッドの場合は1000文字列を解析し、4スレッドはそれぞれ250文字列を解析し、8スレッドはそれぞれ125文字列を解析します。また、スレッドの使用方法をいくつかテストしました。スレッドの作成、スレッドプール、タスク、関数オブジェクトです。

結果: 結果時間はミリ秒単位です。

シングルスレッド:

  • メソッド呼び出し:17720

4スレッド

  • パラメータ化されたスレッドの開始:13836
  • ThreadPool.QueueUserWorkItem:14075
  • Task.Factory.StartNew:16798
  • Func BeginInvoke EndInvoke:16733

8スレッド

  • パラメータ化されたスレッドの開始:12591
  • ThreadPool.QueueUserWorkItem:13832
  • Task.Factory.StartNew:15877
  • Func BeginInvoke EndInvoke:16395

ご覧のとおり、最速はパラメーター化されたスレッドを使用することです。8スレッド(私の論理コアの数)から始めます。ただし、4スレッドを使用した場合に勝るものはなく、シングルコアを使用した場合よりも約29%高速です。もちろん、結果はマシンによって異なります。また、私は

    Dictionary<Type, TypeConverter>

型コンバーターの配列を使用するための文字列解析用のキャッシュでは、パフォーマンスが大幅に向上することはなく、必要なときに配列を作成するよりも、1つの共有キャッシュ型コンバーターを使用する方が保守性が高くなります。

別の更新:

さて、私はさらにいくつかのテストを実行して、パフォーマンスをさらに絞り込めるかどうかを確認しました。興味深いことがいくつか見つかりました。私は8つのスレッドを使用することにしました。これらはすべて、Parameterized Thread Startメソッドから開始されました(これは以前のテストの中で最速でした)。上記と同じテストが実行されましたが、解析アルゴリズムが異なります。きがついた

    Convert.ChangeType and TypeConverter

ほぼ同じ時間がかかります。次のようなタイプ固有のコンバーター

    int.TryParse

私のタイプは動的であるため、少し高速ですが、オプションではありません。ricovoxは、例外処理についていくつかの良いアドバイスをしました。私のデータには確かに無効なデータがあり、一部の整数列は空の数値にダッシュ'-'を付けるので、型コンバーターはその時点で爆発します。つまり、解析するすべての行に少なくとも1つの例外、つまり1000の例外があります。非常に時間がかかります。

ところで、これは私がTypeConverterで変換を行う方法です。拡張機能は単なる静的クラスであり、GetTypeConverterはcahcedTypeConverterを返すだけです。変換中に例外がスローされた場合、デフォルト値が使用されます。

public static Object ConvertTo(this String arg, CultureInfo cultureInfo, Type type, Object defaultValue)
{
  Object value;
  TypeConverter typeConverter = Extensions.GetTypeConverter(type);

  try
  {
    // Try converting the string.
    value = typeConverter.ConvertFromString(null, cultureInfo, arg);
  }
  catch
  {
    // If the conversion fails then use the default value.
    value = defaultValue;
  }

  return value;
}

結果:

8スレッドで同じテスト-1000行、各16列、スレッドあたり250行を解析します。

だから私は3つの新しいことをしました。

1-テストを実行します。例外を最小限に抑えるために、解析する前に既知の無効なタイプを確認します。つまり、if(!Char.IsDigit(c))value = 0; またはcolumnString.Contains('-')など..

ランタイム:29ms

2-テストを実行します。trycatchブロックを持つカスタム解析アルゴリズムを使用します。

ランタイム:12424ms

3-テストを実行します。例外を最小限に抑えるために、解析する前に無効な型をチェックするカスタム解析アルゴリズムを使用します。

ランタイム15ms

わお!ご覧のとおり、例外を排除することで世界に違いが生まれました。例外が本当にどれほど高価なのか、私は気づきませんでした。したがって、例外を本当に未知のケースに最小化すると、解析アルゴリズムは3桁速く実行されます。私はこれが完全に解決されたと考えています。TypeConverterを使用して動的型変換を維持すると思いますが、数ミリ秒遅くなります。変換する前に既知の無効な型をチェックすると、例外が回避され、処理が大幅に高速化されます。これをさらにテストするように指摘してくれたricovoxに感謝します。

4

3 に答える 3

3

主に文字列をネイティブデータ型(string、int、bool、DateTimeなど)に変換する場合は、以下のコードのようなものを使用できます。これは、TypeCodesとTypeConverters(非ネイティブ型の場合)をキャッシュし、 fast switchステートメントを使用して、適切な解析ルーチンにすばやくジャンプします。ソースタイプ(文字列)はすでにわかっているため、Convert.ChangeTypeよりも時間を節約でき、適切な解析メソッドを直接呼び出すことができます。

/* Get an array of Types for each of your columns.
 * Open the data file for reading.
 * Create your DataTable and add the columns.
 * (You have already done all of these in your earlier processing.)
 * 
 * Note:    For the sake of generality, I've used an IEnumerable<string> 
 * to represent the lines in the file, although for large files,
 * you would use a FileStream or TextReader etc.
*/      
IList<Type> columnTypes;        //array or list of the Type to use for each column
IEnumerable<string> fileLines;  //the lines to parse from the file.
DataTable table;                //the table you'll add the rows to

int colCount = columnTypes.Count;
var typeCodes = new TypeCode[colCount];
var converters = new TypeConverter[colCount];
//Fill up the typeCodes array with the Type.GetTypeCode() of each column type.
//If the TypeCode is Object, then get a custom converter for that column.
for(int i = 0; i < colCount; i++) {
    typeCodes[i] = Type.GetTypeCode(columnTypes[i]);
    if (typeCodes[i] == TypeCode.Object)
        converters[i] = TypeDescriptor.GetConverter(columnTypes[i]);
}

//Probably faster to build up an array of objects and insert them into the row all at once.
object[] vals = new object[colCount];
object val;
foreach(string line in fileLines) {
    //delineate the line into columns, however you see fit. I'll assume a tab character.
    var columns = line.Split('\t');
    for(int i = 0; i < colCount) {
        switch(typeCodes[i]) {
            case TypeCode.String:
                val = columns[i]; break;
            case TypeCode.Int32:
                val = int.Parse(columns[i]); break;
            case TypeCode.DateTime:
                val = DateTime.Parse(columns[i]); break;
            //...list types that you expect to encounter often.

            //finally, deal with other objects
            case TypeCode.Object:
            default:
                val = converters[i].ConvertFromString(columns[i]);
                break;
        }
        vals[i] = val;
    }
    //Add all values to the row at one time. 
    //This might be faster than adding each column one at a time.
    //There are two ways to do this:
    var row = table.Rows.Add(vals); //create new row on the fly.
    // OR 
    row.ItemArray = vals; //(e.g. allows setting existing row, created previously)
}

基本的に、型自体によって定義された生の文字列解析メソッドを使用しているだけなので、他に高速な方法はありません。出力タイプごとに独自の解析コードを自分で書き直して、遭遇する正確な形式を最適化することができます。しかし、それはあなたのプロジェクトにとってやり過ぎだと思います。いずれの場合も、FormatProviderまたはNumberStylesを単純に調整する方がおそらくより適切で高速です。

たとえば、Double値を解析するときはいつでも、独自のファイル形式に基づいて、指数などを含む文字列に遭遇することはなく、先頭または末尾にスペースなどがないことがわかっているとします。 。したがって、次のようにNumberStyles引数を使用して、パーサーをこれらのことの手がかりにすることができます。

//NOTE:   using System.Globalization;
var styles = NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
var d = double.Parse(text, styles);

解析がどのように実装されているかはわかりませんが、NumberStyles引数を使用すると、さまざまなフォーマットの可能性を排除することで、解析ルーチンをより高速に動作させることができると思います。もちろん、データの形式について推測できない場合は、これらのタイプの最適化を行うことはできません。

もちろん、文字列を特定のデータ型に解析するのに時間がかかるという理由だけで、コードが遅くなる可能性は常にあります。パフォーマンスアナライザー(VS2010など)を使用して、実際のボトルネックがどこにあるかを確認します。そうすれば、最適化を改善したり、単にあきらめたりすることができます。たとえば、アセンブリで解析ルーチンを作成する以外にやるべきことが他にない場合などです:-)

于 2012-12-13T21:27:49.780 に答える
1

試してみる簡単なコードを次に示します。

Dictionary<Type, TypeConverter> _ConverterCache = new Dictionary<Type, TypeConverter>();

TypeConverter GetCachedTypeConverter(Type type)
{
    if (!_ConverterCache.ContainsKey(type))
        _ConverterCache.Add(type, TypeDescriptor.GetConverter(type));
     return _ConverterCache[type];
}

次に、代わりに以下のコードを使用します。

TypeConverter typeConverter = GetCachedTypeConverter(type);

少し速いですか?

于 2012-12-13T20:29:56.157 に答える
0

私がよく使うテクニックは次のとおりです。

var parserLookup = new Dictionary<Type, Func<string, dynamic>>();

parserLookup.Add(typeof(Int32), s => Int32.Parse(s));
parserLookup.Add(typeof(Int64), s => Int64.Parse(s));
parserLookup.Add(typeof(Decimal), s => Decimal.Parse(s, NumberStyles.Number | NumberStyles.Currency, CultureInfo.CurrentCulture));
parserLookup.Add(typeof(DateTime), s => DateTime.Parse(s, CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal));
// and so on for any other type you want to handle.

Typeこれは、データが何を表しているかを理解できることを前提としています。の使用は、dynamic.net 4以降も意味しますがobject、ほとんどの場合、これを変更できます。

各ファイル(またはアプリ全体)のパーサールックアップをキャッシュすると、かなり良いパフォーマンスが得られるはずです。

于 2012-12-13T22:22:37.007 に答える