12

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

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

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

到目前为止,我已经尝试了以下方法,并且都需要几分钟来解析文件。请注意,如果我注释掉这一行,它只会在几百毫秒内运行。

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

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

如果有人知道像这样通用的更快方法,我想知道它。或者,如果我的整个方法由于某种原因很糟糕,我愿意接受建议。但是请不要向我指出使用硬编码类型的非泛型方法;这根本不是一个选择。

UPDATE - 多线程提高性能测试

为了提高性能,我考虑将解析任务拆分为多个线程。我发现速度有所提高,但仍然没有我希望的那么快。但是,对于那些感兴趣的人,这是我的结果。

系统:

Intel Xenon 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 个字符串。我还测试了几种使用线程的不同方式:线程创建、线程池、任务和函数对象。

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

单线程:

  • 方法调用:17720

4 线程

  • 参数化线程开始:13836
  • ThreadPool.QueueUserWorkItem:14075
  • Task.Factory.StartNew:16798
  • 函数 BeginInvoke EndInvoke:16733

8 个线程

  • 参数化线程开始:12591
  • ThreadPool.QueueUserWorkItem:13832
  • Task.Factory.StartNew:15877
  • 函数 BeginInvoke EndInvoke:16395

如您所见,最快的是使用 8 个线程(我的逻辑核心数)的参数化线程开始。然而,它并没有比使用 4 个线程好太多,并且仅比使用单核快 29%。当然结果会因机器而异。我也坚持了

    Dictionary<Type, TypeConverter>

用于字符串解析的缓存,因为使用类型转换器数组并没有提供显着的性能提升,并且拥有一个共享的缓存类型转换器更易于维护,而不是在需要时到处创建数组。

另一个更新:

好的,所以我进行了更多测试,看看是否可以挤出更多性能,我发现了一些有趣的东西。我决定坚持使用 8 个线程,全部从 Parameterized Thread Start 方法开始(这是我之前测试中最快的)。运行与上述相同的测试,只是使用不同的解析算法。我注意到

    Convert.ChangeType and TypeConverter

花费大约相同的时间。类型特定的转换器,例如

    int.TryParse

稍微快一点,但对我来说不是一个选择,因为我的类型是动态的。ricovox 有一些关于异常处理的好建议。我的数据确实有无效数据,一些整数列会为空数字加上破折号“-”,所以类型转换器会爆炸:这意味着我解析的每一行至少有一个异常,那就是 1000 个异常!非常耗时。

顺便说一句,这就是我使用 TypeConverter 进行转换的方式。Extensions 只是一个静态类,GetTypeConverter 只返回一个cahced TypeConverter。如果在转换过程中抛出异常,则使用默认值。

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('-') 等...

运行时间:29 毫秒

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

运行时间:12424ms

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

运行时间 15 毫秒

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

4

3 回答 3

3

如果您主要将字符串转换为本机数据类型(字符串、int、bool、DateTime 等),您可以使用类似下面的代码,它缓存 TypeCodes 和 TypeConverters(对于非本机类型)并使用快速 switch 语句快速跳转到适当的解析例程。这应该比 Convert.ChangeType 节省一些时间,因为源类型(字符串)是已知的,您可以直接调用正确的 parse 方法。

/* 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您的数据代表什么。的使用也意味着 .net 4 或更高版本,但在大多数情况下dynamic您可以将其更改为。object

缓存每个文件(或整个应用程序)的解析器查找,您应该会获得相当不错的性能。

于 2012-12-13T22:22:37.007 回答