Самый быстрый, эффективный, элегантный способ анализа строк для динамических типов?

Я ищу самый быстрый (общий подход) к преобразованию строк в различные типы данных на ходу.

Я разбираю файлы больших текстовых данных, сгенерированные чем-то (файлы размером несколько мегабайт). Эта особенная функция считывает строки в текстовом файле, анализирует каждую строку в столбцах на основе разделителей и помещает проанализированные значения в .NET DataTable. Это позже вставляется в базу данных. Мое узкое место по 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 Quad Core E3-1245

Память: 12,0 ГБ

Windows 7 Enterprise 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
  • Func BeginInvoke EndInvoke: 16733

8 Темы

  • Параметрированный старт: 12591
  • ThreadPool.QueueUserWorkItem: 13832
  • Task.Factory.StartNew: 15877
  • Func BeginInvoke EndInvoke: 16395

Как вы видите, самый быстрый из них - это параметр Parameterized Thread Start с 8 потоками (число моих логических ядер). Однако он не сильно использует 4 потока, и только на 29% быстрее, чем использование одного ядра. Конечно, результаты будут зависеть от машины. Также я застрял с

    Dictionary<Type, TypeConverter>

кэш для синтаксического анализа строк, поскольку использование массивов преобразователей типов не обеспечивало заметного увеличения производительности, и наличие одного общего кэшированного конвертера типов более удобно, а не создавать массивы повсюду, когда они мне нужны.

ДРУГОЕ ОБНОВЛЕНИЕ:

Итак, я провел еще несколько тестов, чтобы увидеть, могу ли я выжать еще больше производительности, и я нашел несколько интересных вещей. Я решил придерживаться 8 потоков, все началось с метода параметризованного потока старта (который был самым быстрым из моих предыдущих тестов). Тот же тест, что и выше, был запущен, только с различными алгоритмами синтаксического анализа. Я заметил, что

    Convert.ChangeType and TypeConverter

взять примерно такое же количество времени. Тип конкретных преобразователей типа

    int.TryParse

немного быстрее, но не вариант для меня, так как мои типы являются динамическими. У ricovox были хорошие советы по обработке исключений. У моих данных действительно есть недопустимые данные, некоторые целые столбцы помещают тире '-' для пустых чисел, поэтому преобразователи типов взорвутся на это: значение каждой строки, которую я анализирую, имеет как минимум одно исключение, это 1000 исключений! Очень много времени.

Кстати, вот как я делаю свои преобразования с TypeConverter. Расширения - это просто статический класс, и 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 - Запустите тест: проверьте известные недопустимые типы перед разбором, чтобы минимизировать исключения. т.е. если (! Char.IsDigit(c)) value = 0; OR columnString.Contains('-') и т.д.

Время выполнения: 29 мс

2 - Запустите тест: используйте собственные алгоритмы синтаксического анализа, которые имеют блоки try catch.

Время выполнения: 12424мс

3 - Запустите тест: используйте алгоритмы синтаксического анализа для проверки недопустимых типов перед синтаксическим разбором, чтобы минимизировать исключения.

Время выполнения 15мс

Ничего себе! Как вы видите, устранение исключений привело к разнице в мире. Я никогда не понимал, насколько дорогими исключениями были! Таким образом, если я минимизирую свои исключения из TRULY неизвестных случаев, тогда алгоритм синтаксического анализа будет выполняться на три порядка быстрее. Я рассматриваю это абсолютно решительно. Я считаю, что я буду поддерживать динамическое преобразование типов с помощью TypeConverter, это всего на несколько миллисекунд медленнее. Проверка известных недопустимых типов перед конвертированием исключает исключения и ускоряет работу! Благодаря ricovox за то, что я указал, что это заставило меня проверить это дальше.

Ответ 1

если вы в первую очередь собираетесь преобразовывать строки в собственные типы данных (string, int, bool, DateTime и т.д.), вы можете использовать что-то вроде кода ниже, который кэширует TypeCodes и TypeConverters (для не-родных типов) и использует быстрый оператор 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 в каждом случае.

Например, скажем, что всякий раз, когда вы разбираете двойные значения, вы знаете, основываясь на своем собственном формате файла, что вы не столкнетесь ни с какими-либо строками, которые содержат экспоненты и т.д., и вы знаете, что не будет никаких ведущих или конечных пространство и т.д. Итак, вы можете определить парсер в этих вещах с помощью аргумента NumberStyles следующим образом:

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

Я не знаю, как реализован синтаксический анализ, но я думаю, что аргумент NumberStyles позволяет работать с парсингами быстрее, исключая различные возможности форматирования. Конечно, если вы не можете делать какие-либо предположения о формате данных, то вы не сможете сделать эти типы оптимизации.

Конечно, всегда есть вероятность, что ваш код медленный, потому что требуется время для разбора строки в определенный тип данных. Используйте анализатор производительности (например, VS2010), чтобы попытаться определить, где находится ваше фактическое узкое место. Тогда вы сможете оптимизировать лучше или просто отказаться, например. в случае, если есть еще что-то, кроме написания подпрограмм синтаксического анализа в сборке:-)

Ответ 2

Вот фрагмент кода, который можно попробовать:

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);

Есть немного быстрее?

Ответ 3

Обычно я использую технику:

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 в большинстве случаев.

Загрузите поиск парсера для каждого файла (или для всего вашего приложения), и вы получите хорошую производительность.