Действительно ли String.IndexOf (char) медленнее, чем ручной поиск? - PullRequest
4 голосов
/ 03 января 2009

Когда я выполнял простое измерение производительности, я был поражен, увидев, что вызов String.IndexOf(char) был на самом деле медленнее, чем делать это вручную! Это правда? Вот мой тестовый код:

const string str = @"91023m lkajsdfl;jkasdf;piou-09324\\adf \asdf\45\ 65u\ 86\ 8\\\;";
static int testIndexOf() { return str.IndexOf('\\'); }
static int testManualIndexOf() {
    string s = str;
    for (int i = 0; i < s.Length; ++i)
        if (s[i] == '\\') return i;
    return -1;
}

Я запускал каждый метод 25 миллионов раз и измерял время, используя Stopwatch. Ручная версия была на 25% быстрее, чем другая.

Есть мысли?!


Редактировать 2: Запуск тестов на другом компьютере с .NET 4.0 дает результаты, очень похожие на те, что были в ответе Марка Гравелла. String.IndexOf(char) быстрее , чем ручной поиск.


Редактировать: Сегодня (4 января 2009 г.) Я хотел более подробно проверить эту проблему, поэтому создал новый проект и написал код, который вы найдете ниже. Я получил следующие результаты при запуске режима выпуска из cmd для 100 миллионов итераций:

  -   String.      IndexOf : 00:00:07.6490042
  - MyString.PublicIndexOf : 00:00:05.6676471
  - MyString.InternIndexOf : 00:00:06.1191796
  - MyString.PublicIndexOf2: 00:00:09.1363687
  - MyString.InternIndexOf2: 00:00:09.1182569

Я проводил эти тесты не менее 20 раз, получая почти одинаковые результаты каждый раз. Моя машина - XP SP3, VS 2008 SP1, P4 3.0 ГГц без гиперпоточности и 1 ГБ оперативной памяти. Я нахожу результаты действительно странными. Как видите, String.IndexOf был примерно на 33% медленнее, чем мой PublicIndexOf. Даже более странно, я написал тот же метод, что и internal, и он был примерно на 8% медленнее, чем public! Я не понимаю, что происходит, и я надеюсь, что вы могли бы помочь мне понять!

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

public static class MyString {
    public static int PublicIndexOf(string str, char value) {
        for (int i = 0; i < str.Length; ++i)
            if (str[i] == value) return i;
        return -1;
    }

    internal static int InternIndexOf(string str, char value) {
        for (int i = 0; i < str.Length; ++i)
            if (str[i] == value) return i;
        return -1;
    }

    public static int PublicIndexOf2(string str, char value, int startIndex) {
        if (startIndex < 0 || startIndex >= str.Length)
            throw new ArgumentOutOfRangeException("startIndex");
        for (; startIndex < str.Length; ++startIndex)
            if (str[startIndex] == value) return startIndex;
        return -1;
    }

    internal static int InternIndexOf2(string str, char value, int startIndex) {
        if (startIndex < 0 || startIndex >= str.Length)
            throw new ArgumentOutOfRangeException("startIndex");
        for (; startIndex < str.Length; ++startIndex)
            if (str[startIndex] == value) return startIndex;
        return -1;
    }
}

class Program {
    static void Main(string[] args) {
        int iterations = 100 * 1000 * 1000; // 100 millions
        char separator = '\\';
        string str = @"91023m lkajsdfl;jkasdf;piou-09324\\adf \asdf\45\ 65u\ 86\ 8\\\;";
        Stopwatch watch = new Stopwatch();

        // test String.IndexOf
        int sum = 0;
        watch.Start();
        for (int i = 0; i < iterations; ++i)
            sum += str.IndexOf(separator);
        watch.Stop();
        Console.WriteLine("  String.      IndexOf : ({0}, {1})", watch.Elapsed, sum);

        // test MyString.PublicIndexOf
        sum = 0;
        watch.Reset(); watch.Start();
        for (int i = 0; i < iterations; ++i)
            sum += MyString.PublicIndexOf(str, separator);
        watch.Stop();
        Console.WriteLine("MyString.PublicIndexOf : ({0}, {1})", watch.Elapsed, sum);

        // test MyString.InternIndexOf
        sum = 0;
        watch.Reset(); watch.Start();
        for (int i = 0; i < iterations; ++i)
            sum += MyString.InternIndexOf(str, separator);
        watch.Stop();
        Console.WriteLine("MyString.InternIndexOf : ({0}, {1})", watch.Elapsed, sum);

        // test MyString.PublicIndexOf2
        sum = 0;
        watch.Reset(); watch.Start();
        for (int i = 0; i < iterations; ++i)
            sum += MyString.PublicIndexOf2(str, separator,0);
        watch.Stop();
        Console.WriteLine("MyString.PublicIndexOf2: ({0}, {1})", watch.Elapsed, sum);

        // test MyString.InternIndexOf2
        sum = 0;
        watch.Reset(); watch.Start();
        for (int i = 0; i < iterations; ++i)
            sum += MyString.InternIndexOf2(str, separator,0);
        watch.Stop();
        Console.WriteLine("MyString.InternIndexOf2: ({0}, {1})", watch.Elapsed, sum);
    }
}

Ответы [ 10 ]

8 голосов
/ 03 января 2009

Даже если делать это быстрее «вручную», это не разумно. Сегодня это быстрее, может быть, в вашей версии .net, вашей настройке CLR. Может быть. По крайней мере, для этой конкретной строки. Но вы можете быть совершенно уверены, что у встроенного ПО будет больше шансов на улучшение со временем. Иди со своим языком. И IndexOf тоже намного понятнее.

6 голосов
/ 03 января 2009

(обновлено)

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

String.      IndexOf : (00:00:06.3134669, -994967296)
MyString.PublicIndexOf : (00:00:07.0769368, -994967296)
MyString.InternIndexOf : (00:00:08.3463652, -994967296)
MyString.PublicIndexOf2: (00:00:12.0049268, -994967296)
MyString.InternIndexOf2: (00:00:12.4344756, -994967296)

(оригинал)

Я не могу воспроизвести ваши номера в выпуске на консоли. Я сделал 25M итераций, с результатами:

  • 1556мс, (825000000) (testIndexOf)
  • 1798 мс, (825000000) (testManualIndexOf)

Так что IndexOf быстрее. Я подозреваю, что ваша тестовая установка не запускается в режиме выпуска из командной строки (вы не можете запустить тесты производительности с подключенным отладчиком; даже IDE добавляет слишком много служебной информации).

Вот моя установка:

static void Main()
{
    const int LOOP = 25000000;
    int chk1 = 0, chk2 = 0;
    Stopwatch watch1 = Stopwatch.StartNew();
    for (int i = 0; i < LOOP; i++)
    {
        chk1 += testIndexOf();
    }
    watch1.Stop();
    Stopwatch watch2 = Stopwatch.StartNew();
    for (int i = 0; i < LOOP; i++)
    {
        chk2 += testManualIndexOf();
    }
    watch2.Stop();

    Console.WriteLine("{0}ms, ({1})", watch1.ElapsedMilliseconds, chk1);
    Console.WriteLine("{0}ms, ({1})", watch2.ElapsedMilliseconds, chk2);

}
6 голосов
/ 03 января 2009

Ваш тест не совсем честный, поскольку вы не реализуете ту же функциональность:

  1. Вы не передаете никаких аргументов в свой ручной тест, вместо этого используете константную ссылку, которая требует выполнения меньшего количества операционных кодов, чем при передаче аргументов, как в реальном методе IndexOf.
  2. Ваш тест IndexOf фактически выполняет два вызова метода, а не один. Вполне вероятно, что внешний вызов метода будет встроен в сборку Release, не запускаемую под отладчиком, но ваш тест не показывает условий, при которых он был запущен (если это так, вы можете проигнорировать эту точку).

Но, по сути, учитывая, что ваш метод делает меньше, я не удивлен, что он работает быстрее.

6 голосов
/ 03 января 2009

Возможно, эти 25% составляют стоимость вызова функции.

Ваш метод (testManualIndexOf) не вызывает никакой функции.

5 голосов
/ 03 января 2009

Определенно что-то не так с вашим тестом или с вашей средой.

Я просто запускаю этот код:

    static void Main(string[] args)
    {
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 25000000; i++)
                testIndexOf();
            sw.Stop();
            Console.WriteLine(sw.Elapsed); //2.27 s
        }
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 25000000; i++)
                testManualIndexOf();
            sw.Stop();
            Console.WriteLine(sw.Elapsed); //9.81 s
        }

        Console.ReadLine();
    }

на двух разных машинах, одной Vista, одной Linux / mono и обнаружил, что внутренний IndexOf работает в 4 раза быстрее.

2 голосов
/ 03 января 2009

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

2 голосов
/ 03 января 2009

Является ли время для этого конкретного поиска действительно критическим, или вы делаете смертельную "оптимизацию без причины"? Вы пробовали это с более короткими и длинными последовательностями? С разными строками поиска? Встроенная функция может быть оптимизирована для других случаев, когда поиск может быть медленнее, а ваш конкретный случай заканчивается медленнее. Я бы не стал осуждать API IndexOf как «всегда более медленный», пока не проведу еще немного тестирования гораздо большего числа случаев.

2 голосов
/ 03 января 2009

Разве нельзя загрузить код платформы .net и посмотреть, что на самом деле делает str.IndexOf?

1 голос
/ 03 января 2009

Я превратил ваш код в реальный метод и добавил тест для проверки на ноль. Затем я написал цикл для генерации десяти миллионов случайных строк произвольной длины (1 <= length <= 250) и сравнил время, используя метод экземпляра String.IndexOf и пользовательский testManualIndexOf. String.IndexOf потребовалось 5159 миллисекунд, а testManualIndexOf - 4838 миллисекунд при разнице в 6,2%. Ниже приведен не очень красивый код:

public static void Main(string[] args) {
    int nTrials = 10000000;
    Random rg = new Random(1);
    Stopwatch sw = new Stopwatch();
    for (int i = 0; i < nTrials; i++) {
        int len = 1 + rg.Next(250);
        char[] randomChar = new char[len];
        for(int j = 0; j < len; j++) {
            randomChar[j] = GetRandomLetterOrDigit(rg);
        }
        string s = new string(randomChar);
        char findChar = GetRandomLetterOrDigit(rg);
        sw.Start();
        int index = s.IndexOf(findChar);
        //int index = testManualIndexOf(s, findChar);
        sw.Stop();
    }
    Console.WriteLine(sw.ElapsedMilliseconds);
}

private static char GetRandomLetterOrDigit(Random rg) {
    char c;
    int rc = rg.Next(62);

    if (rc < 26) {
        c = (char)('A' + rc);
    }
    else if (rc < 52) {
        c = (char)('a' + rc - 26);
    }
    else {
        c = (char)('0' + rc - 52);
    }
    return c;
}

private static int testManualIndexOf(string s, char c) {
    if (s == null) {
        throw new ArgumentNullException("s");
    }
    for (int i = 0; i < s.Length; ++i) {
        if (s[i] == c) return i;
    }
    return -1;
}
1 голос
/ 03 января 2009

Как уже было сказано, я думаю, что вызов функции может объяснить некоторые различия. Передача и проверка параметров требует времени.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...