Метод не указывается компилятором JIT, хотя кажется, что все критерии выполнены - PullRequest
7 голосов
/ 19 сентября 2019

Фон

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

Пытаясь найти наиболее эффективный код, возможный для достижения этой цели, я настроил несколько тестов, которые показали, что 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, и я предполагаю, что это так же хорошо, как и без добавления условий самостоятельно?

1 Ответ

2 голосов
/ 19 сентября 2019

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

Однако эффективность / скорость выполнения остаются неизменными.Ханс Пассант предложил заменить операторов с коротким замыканием (где это возможно) на обычные | и &, что сокращает разрыв в скорости с 2х до 1,5х.Я предполагаю, что это так же хорошо, как это происходит, когда дело доходит до оптимизации JIT.

return c == '\r' | (c == '\n' & (index == 0 || text[index - 1] != '\r'));

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

ЦельЭто приключение заключалось в том, чтобы получить как можно более эффективный код без необходимости дублировать его везде, где я его использую (поскольку в исходном коде IsEndOfLine() используется несколько раз по всему проекту).В конце концов, я думаю, что я буду придерживаться дублирования кода IsEndOfLine() только внутри GetLineNumber(), поскольку это оказалось самым быстрым с точки зрения скорости выполнения.


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


Полные результаты тестов для дальнейшего использования (по времени исполнения):

Text length: 15882
Character position: 11912

Standard loop (inline):                    <b>00:00:00.1429526 (10000 à 0.0142 ms)</b>
Standard loop (inline unsafe):             00:00:00.1642801 (10000 à 0.0164 ms)
Standard loop (inline + no short-circuit): 00:00:00.3250843 (10000 à 0.0325 ms)
Standard loop (AggressiveInlining):        00:00:00.3318966 (10000 à 0.0331 ms)
Standard loop (unsafe):                    00:00:00.3605394 (10000 à 0.0360 ms)
Standard loop:                             00:00:00.3859629 (10000 à 0.0385 ms)
Regex (Substring):                         00:00:01.8794045 (10000 à 0.1879 ms)
Regex (MatchCollection loop):              00:00:02.4916785 (10000 à 0.2491 ms)

Resulting line: 284

/* "unsafe" is using pointers to access the string's characters */

class Program
{
    const int RUNS = 10000;

    static void Main(string[] args)
    {
        string text = "";
        Random r = new Random();

        //Some words to fill the string with.
        string[] words = new string[] { "Hello", "world", "Inventory.MaxAmount 32", "+QUICKTORETALIATE", "TNT1 AABBCC 6 A_JumpIf(ACS_ExecuteWithResult(460, 0, 0, 0) == 0, \"See0\")" };

        //Various line endings.
        string[] endings = new string[] { "\r\n", "\r", "\n" };



        /*
            Generate text
        */
        int lineCount = r.Next(256, 513);

        for(int l = 0; l < lineCount; l++) {
            int wordCount = r.Next(1, 4);
            text += new string(' ', r.Next(4, 9));

            for(int w = 0; w < wordCount; w++) {
                text += words[wordCount] + (w < wordCount - 1 ? " " : "");
            }

            text += endings[r.Next(0, endings.Length)];
        }

        Console.WriteLine("Text length: " + text.Length);
        Console.WriteLine();



        /*
            Initialize class and stopwatch
        */
        TextParser parser = new TextParser(text);
        Stopwatch sw = new Stopwatch();

        List<int> numbers = new List<int>(); //Using a list to prevent the compiler from optimizing-away the "GetLineNumber" call.



        /*
            Test 1 - Standard loop
        */
        sw.Restart();
        for(int x = 0; x < RUNS; x++) {
            numbers.Add(parser.GetLineNumber((int)(text.Length * 0.75) + r.Next(-4, 4)));
        }
        sw.Stop();

        Console.WriteLine("Line: " + numbers[0]);
        Console.WriteLine("Standard loop: ".PadRight(41) + sw.Elapsed.ToString() + " (" + numbers.Count + " à " + new TimeSpan(sw.Elapsed.Ticks / numbers.Count).TotalMilliseconds.ToString() + " ms)");
        Console.WriteLine();

        numbers = new List<int>();



        /*
            Test 2 - Standard loop (with AggressiveInlining)
        */
        sw.Restart();
        for(int x = 0; x < RUNS; x++) {
            numbers.Add(parser.GetLineNumber2((int)(text.Length * 0.75) + r.Next(-4, 4)));
        }
        sw.Stop();

        Console.WriteLine("Line: " + numbers[0]);
        Console.WriteLine("Standard loop (AggressiveInlining): ".PadRight(41) + sw.Elapsed.ToString() + " (" + numbers.Count + " à " + new TimeSpan(sw.Elapsed.Ticks / numbers.Count).TotalMilliseconds.ToString() + " ms)");
        Console.WriteLine();

        numbers = new List<int>();



        /*
            Test 3 - Standard loop (with inline check)
        */
        sw.Restart();
        for(int x = 0; x < RUNS; x++) {
            numbers.Add(parser.GetLineNumberInline((int)(text.Length * 0.75) + r.Next(-4, 4)));
        }
        sw.Stop();

        Console.WriteLine("Line: " + numbers[0]);
        Console.WriteLine("Standard loop (inline): ".PadRight(41) + sw.Elapsed.ToString() + " (" + numbers.Count + " à " + new TimeSpan(sw.Elapsed.Ticks / numbers.Count).TotalMilliseconds.ToString() + " ms)");
        Console.WriteLine();

        numbers = new List<int>();



        /*
            Test 4 - Standard loop (with inline and no short-circuiting)
        */
        sw.Restart();
        for(int x = 0; x < RUNS; x++) {
            numbers.Add(parser.GetLineNumberInline2((int)(text.Length * 0.75) + r.Next(-4, 4)));
        }
        sw.Stop();

        Console.WriteLine("Line: " + numbers[0]);
        Console.WriteLine("Standard loop (inline + no short-circuit): ".PadRight(41) + sw.Elapsed.ToString() + " (" + numbers.Count + " à " + new TimeSpan(sw.Elapsed.Ticks / numbers.Count).TotalMilliseconds.ToString() + " ms)");
        Console.WriteLine();

        numbers = new List<int>();



        /*
            Test 5 - Standard loop (with unsafe check)
        */
        sw.Restart();
        for(int x = 0; x < RUNS; x++) {
            numbers.Add(parser.GetLineNumberUnsafe((int)(text.Length * 0.75) + r.Next(-4, 4)));
        }
        sw.Stop();

        Console.WriteLine("Line: " + numbers[0]);
        Console.WriteLine("Standard loop (unsafe): ".PadRight(41) + sw.Elapsed.ToString() + " (" + numbers.Count + " à " + new TimeSpan(sw.Elapsed.Ticks / numbers.Count).TotalMilliseconds.ToString() + " ms)");
        Console.WriteLine();

        numbers = new List<int>();



        /*
            Test 6 - Standard loop (with inline + unsafe check)
        */
        sw.Restart();
        for(int x = 0; x < RUNS; x++) {
            numbers.Add(parser.GetLineNumberUnsafeInline((int)(text.Length * 0.75) + r.Next(-4, 4)));
        }
        sw.Stop();

        Console.WriteLine("Line: " + numbers[0]);
        Console.WriteLine("Standard loop (inline unsafe): ".PadRight(41) + sw.Elapsed.ToString() + " (" + numbers.Count + " à " + new TimeSpan(sw.Elapsed.Ticks / numbers.Count).TotalMilliseconds.ToString() + " ms)");
        Console.WriteLine();

        numbers = new List<int>();



        /*
            Test 7 - Regex (with Substring)
        */
        sw.Restart();
        for(int x = 0; x < RUNS; x++) {
            numbers.Add(parser.GetLineNumberRegex((int)(text.Length * 0.75) + r.Next(-4, 4)));
        }
        sw.Stop();

        Console.WriteLine("Line: " + numbers[0]);
        Console.WriteLine("Regex (Substring): ".PadRight(41) + sw.Elapsed.ToString() + " (" + numbers.Count + " à " + new TimeSpan(sw.Elapsed.Ticks / numbers.Count).TotalMilliseconds.ToString() + " ms)");
        Console.WriteLine();

        numbers = new List<int>();



        /*
            Test 8 - Regex (with MatchCollection loop)
        */
        sw.Restart();
        for(int x = 0; x < RUNS; x++) {
            numbers.Add(parser.GetLineNumberRegex2((int)(text.Length * 0.75) + r.Next(-4, 4)));
        }
        sw.Stop();

        Console.WriteLine("Line: " + numbers[0]);
        Console.WriteLine("Regex (MatchCollection loop): ".PadRight(41) + sw.Elapsed.ToString() + " (" + numbers.Count + " à " + new TimeSpan(sw.Elapsed.Ticks / numbers.Count).TotalMilliseconds.ToString() + " ms)");
        Console.WriteLine();

        numbers = new List<int>();



        /*
            Tests completed
        */
        Console.Write("All tests completed. Press ENTER to close...");
        while(Console.ReadKey(true).Key != ConsoleKey.Enter);
    }
}
public class TextParser
{
    private static readonly Regex LineRegex = new Regex("\r\n|\r|\n", RegexOptions.Compiled);

    private string text;

    public TextParser(string text)
    {
        this.text = 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)
    {
        char c = text[index];
        return c == '\r' || (c == '\n' && (index == 0 || text[index - 1] != '\r'));
    }

    /// <summary>
    /// Returns whether the specified character index is the end of a line.
    /// </summary>
    /// <param name="index">The index to check.</param>
    /// <returns></returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool IsEndOfLineAggressiveInlining(int index)
    {
        char c = text[index];
        return c == '\r' || (c == '\n' && (index == 0 || text[index - 1] != '\r'));
    }

    /// <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 IsEndOfLineUnsafe(int index)
    {
        unsafe
        {
            fixed(char* ptr = text) {
                char c = ptr[index];
                return c == '\r' || (c == '\n' && (index == 0 || ptr[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;
    }



    /// <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 GetLineNumber2(int index)
    {
        if(index < 0 || index > text.Length) { throw new ArgumentOutOfRangeException("index"); }

        int lineNumber = 1;
        int end = index;

        index = 0;
        while(index < end) {
            if(IsEndOfLineAggressiveInlining(index)) lineNumber++;
            index++;
        }

        return lineNumber;
    }

    /// <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 GetLineNumberInline(int index)
    {
        if(index < 0 || index > text.Length) { throw new ArgumentOutOfRangeException("index"); }

        int lineNumber = 1;
        int end = index;

        index = 0;
        while(index < end) {
            char c = text[index];
            if(c == '\r' || (c == '\n' && (index == 0 || text[index - 1] != '\r'))) lineNumber++;
            index++;
        }

        return lineNumber;
    }

    /// <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 GetLineNumberInline2(int index)
    {
        if(index < 0 || index > text.Length) { throw new ArgumentOutOfRangeException("index"); }

        int lineNumber = 1;
        int end = index;

        index = 0;
        while(index < end) {
            char c = text[index];
            if(c == '\r' | (c == '\n' & (index == 0 || text[index - 1] != '\r'))) lineNumber++;
            index++;
        }

        return lineNumber;
    }

    /// <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 GetLineNumberUnsafe(int index)
    {
        if(index < 0 || index > text.Length) { throw new ArgumentOutOfRangeException("index"); }

        int lineNumber = 1;
        int end = index;

        index = 0;
        while(index < end) {
            if(IsEndOfLineUnsafe(index)) lineNumber++;
            index++;
        }

        return lineNumber;
    }

    /// <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 GetLineNumberUnsafeInline(int index)
    {
        if(index < 0 || index > text.Length) { throw new ArgumentOutOfRangeException("index"); }

        int lineNumber = 1;
        int end = index;

        unsafe
        {
            fixed(char* ptr = text) {
                index = 0;
                while(index < end) {
                    char c = ptr[index];
                    if(c == '\r' || (c == '\n' && (index == 0 || ptr[index - 1] != '\r'))) lineNumber++;
                    index++;
                }
            }
        }

        return lineNumber;
    }

    /// <summary>
    /// Returns the number of the line at the specified character index. Utilizes a Regex.
    /// </summary>
    /// <param name="index">The index of the character which's line number to get.</param>
    /// <returns></returns>
    public int GetLineNumberRegex(int index)
    {
        return LineRegex.Matches(text.Substring(0, index)).Count + 1;
    }

    /// <summary>
    /// Returns the number of the line at the specified character index. Utilizes a Regex.
    /// </summary>
    /// <param name="index">The index of the character which's line number to get.</param>
    /// <returns></returns>
    public int GetLineNumberRegex2(int index)
    {
        int lineNumber = 1;
        MatchCollection mc = LineRegex.Matches(text);

        for(int y = 0; y < mc.Count; y++) {
            if(mc[y].Index >= index) break;
            lineNumber++;
        }

        return lineNumber;
    }
}
...