Страницы

Поиск по вопросам

четверг, 15 ноября 2018 г.

Помогите разобраться с ошибкой

Есть объект, наследованный от IEnumerable. В методе MoveNext этого класса возникает StackOverflowException на строчке с Regex. Само регулярное выражение ищет совпадения текста в файлах. Что примечательно, на Windows 10 x64 c .Net 4.6 все работает, ошибка возникает на Windows 7 x64 c .Net 4.0.
У меня есть 2 предположения почему так происходит:
Первое: возможно, из-за отличий реализации метода Regex в .Net 4.0 и .Net 4.6 Regex занимает больше памяти в стеке и поэтому падает с exception. Второе: может быть, размер стека в Win10 отличается от такового в Win7.
Как проверить текущий размер стека? Как проверить доступный размер стека? И какие еще могут быть причины данной ошибки?
Количество файлов в IEnumerable, по которым идет поиск — 2760 (_objEnumerator.Count), каждый файл подгружается заранее и хранится в качестве строки в этом самом IEnumerable. Ниже приведен примерный код:
private class MyEnumerator : IEnumerable { public bool MoveNext() { if (_objEnumerator == null) { _objEnumerator = _objects.GetEnumerator(); }
Match m;
if (_current == null) { if (!_objEnumerator.MoveNext()) return false;
m = _regex.Match((_objEnumerator.Current).Text); // (_objEnumerator.Current).Text хранит текст файла, ошибка падает в этой строчке }
if (m.Success) { // код выдающий результат return true; } else { _current = null; return MoveNext(); } } }


Ответ

Проблема, думаю, именно из-за рекурсии (глубина 2700, это много). В .NET начиная, кажется, с 4.5, перешли на новый JIT-компилятор, который умеет определять хвостовую рекурсию, и производить вызов tailcall вместо нормального рекурсивного вызова. В результате стек не забивается.

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

Уточнение: я попробовал код, аналогичный вашему, и он не генерирует в VS 2015/.NET 4.5/x64/Release хвостовой вызов в IL-коде. Значит, ваша проблема может быть не в этом. Я попробую расследовать причину дальше.

Дальнейшее расследование: В текущей версии .NET (4.5) хвостовая рекурсия не требует IL-префикса tail. Пример: вот такой код
class Program { [MethodImpl(MethodImplOptions.NoOptimization)] static void Main(string[] args) { var t = new Program(); t.f(1); Console.ReadKey(); // здесь можно приаттачить отладчик t.f(100000000); }
int f(int iterNo) { new DateTime(2017, 2, 3); // увеличим размер кода функции, чтобы сделать // хвостовую оптимизацию привлекательной для JIT if (iterNo == 0) return 0; else return f(iterNo - 1); } }
генерирует следующий IL, без префикса .tail
.method private hidebysig instance int32 f(int32 iterNo) cil managed { // Code size 28 (0x1c) .maxstack 8 //000021: //000022: int f(int iterNo) //000023: { //000024: new DateTime(2017, 2, 3); // увеличим размер кода функции, чтобы сделать IL_0000: ldc.i4 0x7e1 IL_0005: ldc.i4.2 IL_0006: ldc.i4.3 IL_0007: newobj instance void [mscorlib]System.DateTime::.ctor(int32, int32, int32) IL_000c: pop //000025: // хвостовую оптимизацию привлекательной для JIT //000026: if (iterNo == 0) IL_000d: ldarg.1 IL_000e: brtrue.s IL_0012
//000027: return 0; IL_0010: ldc.i4.0 IL_0011: ret
//000028: else //000029: return f(iterNo - 1); IL_0012: ldarg.0 IL_0013: ldarg.1 IL_0014: ldc.i4.1 IL_0015: sub IL_0016: call instance int32 Tailcall.Program::f(int32) IL_001b: ret } // end of method Program::f
Тем не менее, нативный код для f выглядит так:
24: new DateTime(2017, 2, 3); // увеличим размер кода функции, чтобы сделать push rdi push rsi sub rsp,28h mov rdi,rcx mov esi,edx mov ecx,7E1h mov edx,2 mov r8d,3 call 000007FEF1361730 25: // хвостовую оптимизацию привлекательной для JIT 26: if (iterNo == 0) test esi,esi jne 000007FE932A053D 27: return 0; xor eax,eax add rsp,28h pop rsi pop rdi ret lea edx,[rsi-1] // вычли 1 mov rcx,rdi mov rax,7FE932A0080h add rsp,28h // очистили фрейм pop rsi pop rdi jmp rax // переход вместо возврата
Это означает, что хвостовая рекурсия может быть применена на JIT-уровне, без участия IL-компилятора. Поэтому то, что в .NET 4.5 новый JIT-компилятор, вполне могло помочь.
В любом случае, правильное решение проблемы — переписывание кода итеративным образом.

Комментариев нет:

Отправить комментарий