Подтвердить что ты не робот

Кто-нибудь знает о более быстром способе выполнения String.Split()?

Я читаю каждую строку файла CSV и должен получать отдельные значения в каждом столбце. Поэтому сейчас я просто использую:

values = line.Split(delimiter);

где line - это строка, содержащая значения, разделенные разделителем.

Измеряя производительность моего метода ReadNextRow, я заметил, что он тратит 66% на String.Split, поэтому мне было интересно, знает ли кто-нибудь более быстрый способ сделать это.

Спасибо!

4b9b3361

Ответ 1

Следует отметить, что split() является сомнительным подходом для анализа CSV файлов в случае, если вы встретите запятые в файле, например:

1,"Something, with a comma",2,3

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

Как сказано, split() построена для обработки регулярных выражений, которые, очевидно, более сложны, чем вам нужно (и в любом случае неправильный инструмент для работы с экранированными запятыми). Кроме того, split() создает много временных объектов.

Итак, если вы хотите ускорить его (и мне трудно поверить, что производительность этой части действительно проблема), то вы хотите сделать это вручную, и вы хотите повторно использовать свои объекты буфера, чтобы вы не постоянно создавали объектов и дает работу сборщика мусора, чтобы очистить их.

Алгоритм для этого относительно прост:

  • Остановить каждую запятую;
  • Когда вы нажимаете кавычки, продолжайте, пока не нажмете следующий набор кавычек;
  • Обработать экранированные кавычки (например,\") и, возможно, экранированные запятые (\,).

О, и для того, чтобы дать вам некоторое представление о стоимости регулярного выражения, возник вопрос (Java не С#, но принцип тот же), где кто-то хотел заменить каждый n-й символ на строку. Я предложил использовать replaceAll() для String. Джон Скит вручную закодировал цикл. Из любопытства я сравнил две версии, и он был на порядок лучше.

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

Или, еще лучше, используйте другое оптимизированное решение, подобное быстрый CSV-ридер.

Кстати, в то время как это относится к Java, это относится к производительности регулярных выражений в целом (что является универсальным) и replaceAll() по сравнению с кодировкой с ручным кодированием: Putting char в строку java для каждого N символов.

Ответ 2

Реализация BCL для string.Split на самом деле довольно быстро, я проделал некоторое тестирование здесь, пытаясь отформатировать его, и это непросто.

Но есть одна вещь, которую вы можете сделать и что реализовать это как генератор:

public static IEnumerable<string> GetSplit( this string s, char c )
{
    int l = s.Length;
    int i = 0, j = s.IndexOf( c, 0, l );
    if ( j == -1 ) // No such substring
    {
        yield return s; // Return original and break
        yield break;
    }

    while ( j != -1 )
    {
        if ( j - i > 0 ) // Non empty? 
        {
            yield return s.Substring( i, j - i ); // Return non-empty match
        }
        i = j + 1;
        j = s.IndexOf( c, i, l - i );
    }

    if ( i < l ) // Has remainder?
    {
        yield return s.Substring( i, l - i ); // Return remaining trail
    }
}

Вышеуказанный метод не обязательно быстрее, чем string.Split для небольших строк, но он возвращает результаты по мере их нахождения, это сила ленивой оценки. Если у вас длинные очереди или нужно сохранить память, это путь.

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

Ответ 3

В зависимости от использования вы можете ускорить это, используя Pattern.split вместо String.split. Если у вас есть этот код в цикле (который, как я полагаю, вы, вероятно, делаете, так как кажется, что вы разбираете строки из файла) String.split(String regex) будет вызывать Pattern.compile в вашей строке регулярного выражения каждый раз, когда инструкция цикла выполняет. Чтобы оптимизировать это, Pattern.compile шаблон за пределами цикла, а затем использовать Pattern.split, передавая строку, которую вы хотите разбить, внутри цикла.

Надеюсь, что это поможет

Ответ 4

Я нашел эту реализацию, которая на 30% быстрее, чем блог Деяна Пельцеля. Я оттуда:

Решение

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

public int Split(string value, char separator)
{
    int resultIndex = 0;
    int startIndex = 0;

    // Find the mid-parts
    for (int i = 0; i < value.Length; i++)
    {
        if (value[i] == separator)
        {
            this.buffer[resultIndex] = value.Substring(startIndex, i - startIndex);
            resultIndex++;
            startIndex = i + 1;
        }
    }

    // Find the last part
    this.buffer[resultIndex] = value.Substring(startIndex, value.Length - startIndex);
    resultIndex++;

    return resultIndex;

Как использовать

Класс StringSplitter невероятно прост в использовании, как вы можете видеть в приведенном ниже примере. Просто будьте осторожны, чтобы повторно использовать объект StringSplitter, а не создавать новый экземпляр его в циклах или для одноразового использования. В этом случае было бы лучше использовать juse встроенный String.Split.

var splitter = new StringSplitter(2);
splitter.Split("Hello World", ' ');
if (splitter.Results[0] == "Hello" && splitter.Results[1] == "World")
{
    Console.WriteLine("It works!");
}

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

var splitter = new StringSplitter(2);
var len = splitter.Split("Hello World", ' ');
for (int i = 0; i < len; i++)
{
    Console.WriteLine(splitter.Results[i]);
}

Этот подход имеет свои преимущества и недостатки.

Ответ 5

Вы могли бы подумать, что есть оптимизация, но реальность будет заключаться в том, что вы заплатите за них в другом месте.

Вы можете, например, разделить "себя" и пройти через все символы и обработать каждый столбец, когда вы его встретите, но вы все равно будете копировать все части строки в долгосрочной перспективе.

Одна из оптимизаций, которые мы могли бы сделать в C или С++, например, заменяет все разделители символами '\ 0' и сохраняет указатели до начала столбца. Тогда нам не пришлось бы копировать все строковые данные, чтобы добраться до их части. Но этого вы не можете сделать в С#, и не хотите.

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

Мне сказали, что 90% процессорного времени потрачено на 10% кода. Существуют вариации этой "истины". На мой взгляд, потратить 66% вашего времени в Split не так уж плохо, если обработка CSV - это то, что нужно вашему приложению.

Dave

Ответ 7

Основная проблема (?) с String.Split заключается в том, что она является общей, поскольку она удовлетворяет многие потребности.

Если вы знаете больше о своих данных, чем в Split, это может сделать улучшение, чтобы сделать свой собственный.

Например, если:

  • Вам не нужны пустые строки, поэтому вам не нужно обращаться с ними каким-либо специальным способом.
  • Вам не нужно обрезать строки, поэтому вам не нужно ничего делать с ними или рядом с ними.
  • Вам не нужно проверять цитируемые запятые или кавычки
  • Вам вообще не нужно обрабатывать кавычки

Если любое из них истинно, вы можете увидеть улучшение, написав собственную более конкретную версию String.Split.

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

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

Однако, если, скажем, вы заполняете данные в базу данных, тогда 66% времени вашего кода, потраченного на String.Split, представляет большую проблему.

Ответ 8

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

Рекомендуемое решение ODBC, на первый взгляд, похоже на тот же подход.

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

Ответ 9

Как говорили другие, String.Split() не всегда будет хорошо работать с файлами CSV. Рассмотрим файл, который выглядит так:

"First Name","Last Name","Address","Town","Postcode"
David,O'Leary,"12 Acacia Avenue",London,NW5 3DF
June,Robinson,"14, Abbey Court","Putney",SW6 4FG
Greg,Hampton,"",,
Stephen,James,"""Dunroamin"" 45 Bridge Street",Bristol,BS2 6TG

(например, непоследовательное использование речевых меток, строк, включая запятые и речевые метки и т.д.)

Эта структура чтения CSV будет касаться всего этого, а также очень эффективна:

LumenWorks.Framework.IO.Csv от Sebastien Lorien

Ответ 10

Это мое решение:

Public Shared Function FastSplit(inputString As String, separator As String) As String()
        Dim kwds(1) As String
        Dim k = 0
        Dim tmp As String = ""

        For l = 1 To inputString.Length - 1
            tmp = Mid(inputString, l, 1)
            If tmp = separator Then k += 1 : tmp = "" : ReDim Preserve kwds(k + 1)
            kwds(k) &= tmp
        Next

        Return kwds
End Function

Вот версия с бенчмаркингом:

Public Shared Function FastSplit(inputString As String, separator As String) As String()
        Dim sw As New Stopwatch
        sw.Start()
        Dim kwds(1) As String
        Dim k = 0
        Dim tmp As String = ""

        For l = 1 To inputString.Length - 1
            tmp = Mid(inputString, l, 1)
            If tmp = separator Then k += 1 : tmp = "" : ReDim Preserve kwds(k + 1)
            kwds(k) &= tmp
        Next
        sw.Stop()
        Dim fsTime As Long = sw.ElapsedTicks

        sw.Start()
        Dim strings() As String = inputString.Split(separator)
        sw.Stop()

        Debug.Print("FastSplit took " + fsTime.ToString + " whereas split took " + sw.ElapsedTicks.ToString)

        Return kwds
End Function

Ниже приведены некоторые результаты относительно относительно небольших строк, но с различными размерами, до 8kb блоков. (время в тиках)

FastSplit занял 8, а split занял 10

FastSplit занял 214, тогда как split занял 216

FastSplit занял 10, а split занял 12

FastSplit взял 8, тогда как split занял 9

FastSplit занял 8, а split занял 10

FastSplit занял 10, а split занял 12

FastSplit взял 7, тогда как split занял 9

FastSplit занял 6, а split занял 8

FastSplit занял 5, а split занял 7

FastSplit занял 10, тогда как split занял 13

FastSplit занял 9, а split занял 232

FastSplit взял 7, тогда как split занял 8

FastSplit взял 8, тогда как split занял 9

FastSplit занял 8, а split занял 10

FastSplit взял 215, тогда как split занял 217

FastSplit занял 10, тогда как split занял 231

FastSplit занял 8, а split занял 10

FastSplit занял 8, а split занял 10

FastSplit взял 7, тогда как split занял 9

FastSplit занял 8, а split занял 10

FastSplit занял 10, а split занял 1405

FastSplit занял 9, а split занял 11

FastSplit занял 8, а split занял 10

Кроме того, я знаю, что кто-то будет препятствовать моему использованию ReDim Preserve вместо использования списка... Причина в том, что в списке действительно не было разницы в скорости в моих тестах, поэтому я вернулся к "простому" способу.

Ответ 11

    public static unsafe List<string> SplitString(char separator, string input)
    {
        List<string> result = new List<string>();
        int i = 0;
        fixed(char* buffer = input)
        {
            for (int j = 0; j < input.Length; j++)
            {
                if (buffer[j] == separator)
                {
                    buffer[i] = (char)0;
                    result.Add(new String(buffer));
                    i = 0;
                }
                else
                {
                    buffer[i] = buffer[j];
                    i++;
                }
            }
            buffer[i] = (char)0;
            result.Add(new String(buffer));
        }
        return result;
    }

Ответ 12

Можно предположить, что String.Split будет близок к оптимальному; т.е. его было бы довольно сложно улучшить. Напротив, проще решить, нужно ли вообще разбить строку. Весьма вероятно, что вы будете использовать отдельные строки напрямую. Если вы определяете класс StringShim (ссылка на String, begin и end index), вы можете разделить String на набор прокладок. Они будут иметь небольшой фиксированный размер и не будут вызывать строковые копии данных.

Ответ 13

String.split довольно медленный, если вам нужны более быстрые методы, вы здесь.:)

Однако CSV намного лучше разбирается с помощью синтаксического анализа на основе правил.

Этот парень, создал основанный на правилах токенизатор для java. (к сожалению, требуется скопировать и вставить)

http://www.csdgn.org/code/rule-tokenizer

private static final String[] fSplit(String src, char delim) {
    ArrayList<String> output = new ArrayList<String>();
    int index = 0;
    int lindex = 0;
    while((index = src.indexOf(delim,lindex)) != -1) {
        output.add(src.substring(lindex,index));
        lindex = index+1;
    }
    output.add(src.substring(lindex));
    return output.toArray(new String[output.size()]);
}

private static final String[] fSplit(String src, String delim) {
    ArrayList<String> output = new ArrayList<String>();
    int index = 0;
    int lindex = 0;
    while((index = src.indexOf(delim,lindex)) != -1) {
        output.add(src.substring(lindex,index));
        lindex = index+delim.length();
    }
    output.add(src.substring(lindex));
    return output.toArray(new String[output.size()]);
}