C# 以最快、高效、优雅的方式将字符串解析为动态类型?

C# 以最快、高效、优雅的方式将字符串解析为动态类型?,c#,string,performance,typeconverter,C#,String,Performance,Typeconverter,我正在寻找最快的(通用方法)将字符串转换为各种数据类型 我正在解析由某物生成的大型文本数据文件(文件大小为几兆字节)。这个特殊函数读取文本文件中的行,根据定界符将每行解析为列,并将解析后的值放入.NET数据表中。这将稍后插入到数据库中。到目前为止,我的瓶颈是字符串转换(Convert和TypeConverter) 我必须采用动态方式(即远离“Convert.ToInt32”等),因为我永远不知道文件中将包含哪些类型。该类型由运行时的早期配置确定 到目前为止,我已经尝试了以下两种方法,它们都需要几

我正在寻找最快的(通用方法)将字符串转换为各种数据类型

我正在解析由某物生成的大型文本数据文件(文件大小为几兆字节)。这个特殊函数读取文本文件中的行,根据定界符将每行解析为列,并将解析后的值放入.NET数据表中。这将稍后插入到数据库中。到目前为止,我的瓶颈是字符串转换(Convert和TypeConverter)

我必须采用动态方式(即远离“Convert.ToInt32”等),因为我永远不知道文件中将包含哪些类型。该类型由运行时的早期配置确定

到目前为止,我已经尝试了以下两种方法,它们都需要几分钟来解析一个文件。注意 如果我注释掉这一行,它只需要几百毫秒

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

如果有人知道这样一种通用的更快的方法,我想知道。或者,如果我的整个方法因为某种原因而糟糕透顶,我愿意接受建议。但请不要告诉我使用硬编码类型的非泛型方法;这根本不是一个选择

更新-多线程以改进性能测试

为了提高性能,我研究了将解析任务拆分为多个线程。我发现速度有所提高,但仍然没有我希望的那么快。然而,以下是我的结果,供感兴趣的人参考

系统:

英特尔氙气3.3GHz四核E3-1245

内存:12.0 GB

Windows 7企业版x64

测试:

测试功能如下:

(1) 接收字符串数组。(2) 用定界器拆分字符串。(3) 将字符串解析为数据类型并将其存储在一行中。(4) 将行添加到数据表中。(5) 重复(2)-(4)直到完成

该测试包括1000个字符串,每个字符串被解析为16列,因此总共有16000个字符串转换。我测试了单线程、4线程(因为四核)和8线程(因为超线程)。因为我只是在这里处理数据,所以我怀疑添加更多的线程是否会有任何好处。因此,对于单个线程,它解析1000个字符串,4个线程分别解析250个字符串,8个线程分别解析125个字符串。我还测试了几种不同的线程使用方法:线程创建、线程池、任务和函数对象

结果: 结果时间以毫秒为单位

row[i] = Convert.ChangeType(columnString, dataType);
单线程:

  • 方法调用: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>
花费的时间大致相同。特定类型转换器,如

    int.TryParse
稍微快一点,但不是我的选择,因为我的类型是动态的。ricovox对异常处理有一些很好的建议。我的数据确实有无效数据,一些整型列会在空数字上加一个破折号“-”,因此类型转换器在这个位置爆炸:这意味着我分析的每一行至少有一个异常,即1000个异常!非常耗时

顺便说一句,这是我如何用TypeConverter进行转换的。扩展只是一个静态类,GetTypeConverter只返回一个cahced类型转换器。如果在转换过程中引发异常,则使用默认值

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-运行测试:在解析之前检查已知的无效类型,以最小化异常。 i、 e.如果(!Char.IsDigit(c))值=0;或列字符串。包含('-')等

运行时间:29毫秒

2-运行测试:使用具有try-catch块的自定义解析算法

运行时间:12424ms

3-运行测试:使用自定义解析算法在解析之前检查无效类型,以最小化异常

运行时间15毫秒


哇!正如您所看到的,消除异常带来了巨大的不同。我从来没有意识到例外情况到底有多昂贵!因此,如果我将异常最小化到真正未知的情况,那么解析算法的运行速度将提高三个数量级。我认为这个问题已经彻底解决了。我相信我会用TypeConverter保持动态类型转换,它只会慢几毫秒。在转换之前检查已知的无效类型可以避免异常,这将大大加快速度!感谢ricovox指出了这一点,这让我进一步测试了这一点。

这里有一段快速的代码可以尝试:

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];
}

是快一点吗?

如果主要是将字符串转换为本机数据类型(字符串、int、bool、DateTime等),可以使用下面的代码,它缓存类型代码和类型转换器(对于非本机类型),并使用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)
}
我不知道解析是如何实现的,但我认为NumberStyles参数通过排除各种格式化可能性,使解析例程工作得更快。当然,如果不能对数据的格式做出任何假设,那么
TypeConverter typeConverter = GetCachedTypeConverter(type);
/* 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)
}
//NOTE:   using System.Globalization;
var styles = NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
var d = double.Parse(text, styles);
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.