Самый быстрый, эффективный, элегантный способ анализа строк для динамических типов?
Я ищу самый быстрый (общий подход) к преобразованию строк в различные типы данных на ходу.
Я разбираю файлы больших текстовых данных, сгенерированные чем-то (файлы размером несколько мегабайт). Эта особенная функция считывает строки в текстовом файле, анализирует каждую строку в столбцах на основе разделителей и помещает проанализированные значения в .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 строк каждый. Также я проверил несколько различных способов использования потоков: создание потоков, пул потоков, задачи и объекты функций.
Результаты:
Время выполнения - миллисекунды.
Одиночная тема:
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
в большинстве случаев.
Загрузите поиск парсера для каждого файла (или для всего вашего приложения), и вы получите хорошую производительность.