Фон
При написании класса для разбора определенного текста мне понадобилась возможность получить номер строки определенной позиции символа (другими словами, посчитать все разрывы строк, которые встречаются до этого символа).
Пытаясь найти наиболее эффективный код, возможный для достижения этой цели, я настроил несколько тестов, которые показали, что Regex был самым медленным методом и что ручная итерация строки была самой быстрой.
Ниже приводится мойтекущий подход (10 000 итераций: 278 мс ):
private string text;
/// <summary>
/// Returns whether the specified character index is the end of a line.
/// </summary>
/// <param name="index">The index to check.</param>
/// <returns></returns>
private bool IsEndOfLine(int index)
{
//Matches "\r" and "\n" (but not "\n" if it's preceded by "\r").
char c = text[index];
return c == '\r' || (c == '\n' && (index == 0 || text[index - 1] != '\r'));
}
/// <summary>
/// Returns the number of the line at the specified character index.
/// </summary>
/// <param name="index">The index of the character which's line number to get.</param>
/// <returns></returns>
public int GetLineNumber(int index)
{
if(index < 0 || index > text.Length) { throw new ArgumentOutOfRangeException("index"); }
int lineNumber = 1;
int end = index;
index = 0;
while(index < end) {
if(IsEndOfLine(index)) lineNumber++;
index++;
}
return lineNumber;
}
Однако, выполняя эти тесты, я вспомнил, что вызовы методов иногда могут быть немного дороже, поэтому я решил попробовать переместитьусловия из IsEndOfLine()
непосредственно в if
-составление внутри GetLineNumber()
.
Как я и ожидал, это выполняется более чем в два раза быстрее (10 000 итераций: 112 мс ):
while(index < end) {
char c = text[index];
if(c == '\r' || (c == '\n' && (index == 0 || text[index - 1] != '\r'))) lineNumber++;
index++;
}
Проблема
Из того, что я прочитал, JIT-компилятор не оптимизирует (или, по крайней мере, не ) код ILразмером более 32 байт [1] , если не указано [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
[2] .Но, несмотря на применение этого атрибута к IsEndOfLine()
, никакого встраивания, похоже, не происходит.
Большинство сообщений, которые мне удалось найти по этому поводу, относятся к более ранним постам / статьям.В самой последней ( [2] от 2012 г.) автор, по-видимому, успешно встроил 34-байтовую функцию, используя MethodImplOptions.AggressiveInlining
, подразумевая, что флаг позволяет встроить больший код IL, если выполнены все другие критерии.
Измерение размера моего метода с использованием следующего кода показало, что его длина составляет 54 байта:
Console.WriteLine(this.GetType().GetMethod("IsEndOfLine").GetMethodBody().GetILAsByteArray().Length);
При использовании окна Dissasembly в VS 2019 показан следующий код сборкидля IsEndOfLine()
(с исходным кодом C #, включенным в Параметры просмотра ):
(Конфигурация: Release (x86) , отключено Just My Code и Подавить оптимизацию JIT при загрузке модуля )
--- [PATH REMOVED]\Performance Test - Find text line number\TextParser.cs
28: char c = text[index];
001E19BA in al,dx
001E19BB mov eax,dword ptr [ecx+4]
001E19BE cmp edx,dword ptr [eax+4]
001E19C1 jae 001E19FF
001E19C3 movzx eax,word ptr [eax+edx*2+8]
29: return c == '\r' || (c == '\n' && (index == 0 || text[index - 1] != '\r'));
001E19C8 cmp eax,0Dh
001E19CB je 001E19F8
001E19CD cmp eax,0Ah
001E19D0 jne 001E19F4
001E19D2 test edx,edx
001E19D4 je 001E19ED
001E19D6 dec edx
001E19D7 mov eax,dword ptr [ecx+4]
001E19DA cmp edx,dword ptr [eax+4]
001E19DD jae 001E19FF
001E19DF cmp word ptr [eax+edx*2+8],0Dh
001E19E5 setne al
001E19E8 movzx eax,al
001E19EB pop ebp
001E19EC ret
001E19ED mov eax,1
001E19F2 pop ebp
001E19F3 ret
001E19F4 xor eax,eax
001E19F6 pop ebp
001E19F7 ret
001E19F8 mov eax,1
001E19FD pop ebp
001E19FE ret
001E19FF call 70C2E2B0
001E1A04 int 3
... и следующий код для цикла в GetLineNumber()
:
63: index = 0;
001E1950 xor esi,esi
64: while(index < end) {
001E1952 test ebx,ebx
001E1954 jle 001E196C
001E1956 mov ecx,edi
001E1958 mov edx,esi
001E195A call dword ptr ds:[144E10h]
001E1960 test eax,eax
001E1962 je 001E1967
65: if(IsEndOfLine(index)) lineNumber++;
001E1964 inc dword ptr [ebp-10h]
66: index++;
001E1967 inc esi
64: while(index < end) {
001E1968 cmp esi,ebx
001E196A jl 001E1956
67: }
68:
69: return lineNumber;
001E196C mov eax,dword ptr [ebp-10h]
001E196F pop ecx
001E1970 pop ebx
001E1971 pop esi
001E1972 pop edi
001E1973 pop ebp
001E1974 ret
Я не очень хорошо читаю ассемблерный код, но мне кажется, что никакого встраивания не произошло.
Вопрос
Почему JIT-компилятор не включает мой метод IsEndOfLine()
, даже когдаMethodImplOptions.AggressiveInlining
указано?Я знаю, что этот флаг является лишь подсказкой для компилятора, но на основании [2] его применение должно позволить встроить IL больше , чем 32 байта.Кроме того, мне кажется, что мой код удовлетворяет всем остальным условиям.
Существуют ли какие-то другие ограничения, которые мне не хватает?
Тесты
Результаты :
Text length: 11645
Line: 201
Standard loop: 00:00:00.2779946 (10000 à 00:00:00.0000277)
Line: 201
Standard loop (inline): 00:00:00.1122908 (10000 à 00:00:00.0000112)
Сноски
1 Встроить или нет Встроить: Это вопрос
2 Агрессивное встраивание в CLR 4.5 JIT
- РЕДАКТИРОВАТЬ -
По какой-то причине после перезапуска VS, включения и повторного отключения настроек, упомянутых ранее, а также повторного применения MethodImplOptions.AggressiveInlining
, метод теперь выглядит так:быть встроенным.Тем не менее, он добавил пару инструкций, которых нет, когда вы вставляете if
-условия вручную.
JIT-оптимизированная версия :
66: while(index < end) {
001E194B test ebx,ebx
001E194D jle 001E1998
001E194F mov esi,dword ptr [ecx+4]
67: if(IsEndOfLine(index)) lineNumber++;
001E1952 cmp edx,esi
001E1954 jae 001E19CA
001E1956 movzx eax,word ptr [ecx+edx*2+8]
001E195B cmp eax,0Dh
001E195E je 001E1989
001E1960 cmp eax,0Ah
001E1963 jne 001E1985
001E1965 test edx,edx
001E1967 je 001E197E
001E1969 mov eax,edx
001E196B dec eax
001E196C cmp eax,esi
001E196E jae 001E19CA
001E1970 cmp word ptr [ecx+eax*2+8],0Dh
001E1976 setne al
001E1979 movzx eax,al
001E197C jmp 001E198E
001E197E mov eax,1
001E1983 jmp 001E198E
001E1985 xor eax,eax
001E1987 jmp 001E198E
001E1989 mov eax,1
001E198E test eax,eax
001E1990 je 001E1993
001E1992 inc edi
68: index++;
Моя оптимизированная версия :
87: while(index < end) {
001E1E9B test ebx,ebx
001E1E9D jle 001E1ECE
001E1E9F mov esi,dword ptr [ecx+4]
88: char c = text[index];
001E1EA2 cmp edx,esi
001E1EA4 jae 001E1F00
001E1EA6 movzx eax,word ptr [ecx+edx*2+8]
89: if(c == '\r' || (c == '\n' && (index == 0 || text[index - 1] != '\r'))) lineNumber++;
001E1EAB cmp eax,0Dh
001E1EAE je 001E1EC8
001E1EB0 cmp eax,0Ah
001E1EB3 jne 001E1EC9
001E1EB5 test edx,edx
001E1EB7 je 001E1EC8
001E1EB9 mov eax,edx
001E1EBB dec eax
001E1EBC cmp eax,esi
001E1EBE jae 001E1F00
001E1EC0 cmp word ptr [ecx+eax*2+8],0Dh
001E1EC6 je 001E1EC9
001E1EC8 inc edi
90: index++;
Новые инструкции :
001E1976 setne al
001E1979 movzx eax,al
001E197C jmp 001E198E
001E197E mov eax,1
001E1983 jmp 001E198E
001E1985 xor eax,eax
001E1987 jmp 001E198E
001E1989 mov eax,1
001E198E test eax,eax
Я все еще не вижу улучшения в производительности / скорости выполнения, однако... Возможно, это связано с дополнительными инструкциями, которые добавил JIT, и я предполагаю, что это так же хорошо, как и без добавления условий самостоятельно?