Страницы

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

воскресенье, 22 декабря 2019 г.

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

#c_sharp #net #регулярные_выражения


Есть объект, наследованный от 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();
        }
    }
}

    


Ответы

Ответ 1



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

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

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