Есть объект, наследованный от 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-компилятор, вполне могло помочь.
В любом случае, правильное решение проблемы — переписывание кода итеративным образом.
Комментариев нет:
Отправить комментарий