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

Почему мой string.indexof(char) быстрее?

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

public static unsafe int IndexOf16(string s, int startIndex, char c) {
            if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex");
            fixed (char* cs = s) {
                for (int i = startIndex; i < s.Length; i++) {
                    if ((cs[i]) == c) return i;
                }
                return -1;
            }
        }

быстрее, чем string.IndexOf(char). Я написал несколько простых тестов, и, похоже, он точно соответствует результату. Некоторые примеры выходных чисел с моей машины (она, конечно, варьируется, но тренд ясен):

short haystack 500k runs
1741 ms for IndexOf16
2737 ms for IndexOf32
2963 ms for IndexOf64
2337 ms for string.IndexOf <-- buildin

longer haystack:
2888 ms for IndexOf16
3028 ms for IndexOf32
2816 ms for IndexOf64
3353 ms for string.IndexOf <-- buildin

IndexOfChar отмечен extern, поэтому вы не можете его отразить. Однако я считаю, что это должна быть (родная) реализация: http://www.koders.com/cpp/fidAB4768BA4DF45482A7A2AA6F39DE9C272B25B8FE.aspx?s=IndexOfChar#L1000

Они, похоже, используют ту же самую наивную реализацию.

Вопросы приходят мне на ум:

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

2) Я предположил, что большая часть низкоуровневых методов в конечном итоге будет реализована в ручном ассемблере, что, похоже, не так. Если да, зачем реализовать его изначально вообще, а не только в С#, как в моей реализации примера?

(Полный тест здесь (я думаю, его слишком долго, чтобы вставить здесь): http://paste2.org/p/1606018)

(Нет, это не преждевременная оптимизация, это не для проекта, в котором я просто возился): -)

Обновить: Thnx to Oliver для подсказки о nullcheck и параметре Count. Я добавил их в свою реализацию IndexOf16Im так:

public static unsafe int IndexOf16(string s, int startIndex, char c, int count = -1) {
    if (s == null) throw new ArgumentNullException("s");
    if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex");
    if (count == -1) count = s.Length - startIndex;
    if (count < 0 || count > s.Length - startIndex) throw new ArgumentOutOfRangeException("count");

    int endIndex = startIndex + count;
    fixed (char* cs = s) {
        for (int i = startIndex; i < endIndex; i++) {
            if ((cs[i]) == c) return i;
        }
        return -1;
    }
}

Номера немного изменились, однако они все еще довольно значительно быстрее (результаты 32/64 опущены):

short haystack 500k runs
1908 ms for IndexOf16
2361 ms for string.IndexOf
longer haystack:
3061 ms for IndexOf16
3391 ms for string.IndexOf

Update2. Эта версия работает быстрее (особенно для длинного случая сена):

public static unsafe int IndexOf16(string s, int startIndex, char c, int count = -1) {
            if (s == null) throw new ArgumentNullException("s");
            if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex");
            if (count == -1) count = s.Length - startIndex;
            if (count < 0 || count > s.Length - startIndex) throw new ArgumentOutOfRangeException("count");

            int endIndex = startIndex + count;
            fixed (char* cs = s) {
                char* cp = cs + startIndex;
                for (int i = startIndex; i <= endIndex; i++, cp++) {
                    if (*cp == c) return i;
                }
                return -1;
            }
        }

Обновление 4: Основываясь на обсуждении с LastCoder, я считаю, что это зависит от архитектуры. Мой Xeon W3550 на работах, похоже, предпочитает эту версию, в то время как его i7, похоже, напоминает версию buildin. Моя домашняя машина (Athlon II) находится между ними. Однако я удивлен большой разницей.

4b9b3361

Ответ 1

Возможность 1) Это может не выполняться (как истинно) в С#, но когда я делал оптимизацию для ассемблера x86-64, я быстро обнаружил, сравнивая, что код вызова из DLL (отмеченный внешний) был медленнее, чем реализация той же самой точной функции в моем исполняемом файле. Наиболее очевидной причиной является пейджинг и память, DLL (внешний) метод загружен далеко в памяти от остальной части кода, и если он не был ранее доступен, его нужно будет выгрузить. Ваш код бенчмаркинга должен делать некоторые прогрессивные циклы функций, которые вы сравниваете, чтобы убедиться, что их выгружают в память, прежде чем вы их запустите.

Возможность 2) Microsoft стремится не оптимизировать строковые функции в полной мере, поэтому оптимизация собственной длины строки, подстроки, индекса и т.д. На самом деле неслыханно. Анекдот; в ассемблере x86-64 мне удалось создать версию функции RtlInitUnicodeString WinXP64, которая выполнялась в два раза быстрее практически во всех практических случаях.

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


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

            char* cp = cs + startIndex;
            char* cpEnd = cp + endIndex;
            while (cp <= cpEnd) {
                if (*cp == c) return cp - cs;
                cp++;
            }

edit В ответ на (2) для вашего любопытства, закодированный в 2005 году и используемый для исправления ntdll.dll моей машины WinXP64. http://board.flatassembler.net/topic.php?t=4467

RtlInitUnicodeString_Opt: ;;rcx=buff rdx=ucharstr 77bytes
             xor    r9d,r9d
             test   rdx,rdx
             mov    dword[rcx],r9d
             mov    [rcx+8],rdx
             jz     .end
             mov    r8,rdx
   .scan:
             mov    eax,dword[rdx]

             test   ax,ax
             jz     .one
             add    rdx,4
             shr    eax,16
             test   ax,ax
             jz     .two
             jmp    .scan
   .two:
             add    rdx,2
   .one:
             mov    eax,0fffch
             sub    rdx,r8
             cmp    rdx,0fffeh
             cmovnb rdx,rax
             mov    [ecx],dx
             add    dx,2
             mov    [ecx+2],dx
             ret
   .end:
             retn  

edit 2 Запуск кода вашего примера (обновленного с помощью вашей самой быстрой версии) string.IndexOf работает быстрее на моем Intel i7, 4 ГБ оперативной памяти, Win7 64bit.

short haystack 500k runs
2590 ms for IndexOf16
2287 ms for string.IndexOf
longer haystack:
3549 ms for IndexOf16
2757 ms for string.IndexOf

Оптимизации иногда очень зависят от архитектуры.

Ответ 2

Если вы действительно сделаете такую ​​проверку микрометра, каждый бит подсчитывает. В рамках реализации MS (как показано в приведенной ссылке) они также проверяют, является ли s значением null и выбрасывает исключение NullArgumentException. Также это реализация, включая параметр count. Поэтому они дополнительно проверяют, считают ли они правильное значение и бросают исключение ArgumentOutOfRangeException.

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

Ответ 3

Это может иметь какое-то отношение к "фиксированному" утверждению как "Он связывает местоположение объектов src и dst в памяти, чтобы они не были перемещены сбором мусора". возможно, ускорить методы?

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

Выше комментарии взяты из MSDN