(제목수정) streamReader.ReadLine() 의 속도를 높이기 위한 질문과 삽질의 기록

안녕하세요!
오랜만에 밤을 새우면서 프로그램을 만지작 거리고 있습니다.
너무 졸린데! 너무 재미있습니다!!

간략하게 해본게 있는데,
50만줄정도되는 텍스트파일이 있습니다.

이 파일이 생성된 실제 시간은 5분 20초인데,
이걸 5분 20초보다 먼저 프로그램이 해석하게 하는게 제 목표에요!

이걸 위해서, 제가 해본 것들과 차이에 대해서 먼저 말씀올리겠습니다.

public void ParseLine(String line)
{ 
    try
    {
        if (String.IsNullOrEmpty(line))
        {
            return;
        }
        MatchCollection matches;
        matches = 차단했습니다Regex.Matches(line);
        if (matches.Count > 0)
        {
            DateTime time = matches[0].Groups["time"].Value.GetTime(LogTimeFormat);
            String name = matches[0].Groups["name"].Value;
            InstanceDungeonEvent(this, new InstanceDungeonEventArgs(line, time, 21, name, from));
            return;
        }
        matches = 기타등등Regex.Matches(line);
        if (matches.Count > 0)
        {
            DateTime time = matches[0].Groups["time"].Value.GetTime(LogTimeFormat);
            String skill = matches[0].Groups["skill"].Value;
            InstanceDungeonEvent(this, new InstanceDungeonEventArgs(line, time, 21, name, from));
            return; //이 리턴에 대한 글을쓰려고 합니다!!!!
            //이 리턴에 대해서!!!
        }
        //계속 한 30개즘 반복

위에 있는 리턴이 (위치가 다른 몇가지 더 있음) 신기하게도 (바보같게도)
있어도 문제없이 작동하고, 없어도 문제없이 작동하더라구요.
다만, 있어야될(?)위치에 없는 경우에 속도차이가 발생하는 것 같습니다.
제가 생각해도 제가 무지해서 생기는 문제인것 같습니다.
아무튼 그렇게 수도없이 여기도 저기도 리턴을 넣어보고
또 빼보고 하면서 계속 실사용 테스트를… ㅠ_ㅠ
그렇게 나름 1분 걸리는 시간을 5~10초 가까이 줄였습니다!

정확한건 아니지만 빌드를 디버그로 했을때와 릴리즈로 했을때도
대략 1~5초 정도의 시간을 줄였습니다.

이후로 별 쓰잘데기 이상한 것들을 시도하고…
(효과가 있다고 하는데, 제가 잘못해서 그런건지)
그… if 문에서 && 앞뒤를 바꿔본다던지… (대충 이해는 하고 햇는데 큰차이가…ㅠㅠ)

private void StartWorker()
{
    int SleepTime = (IsFile) ? 1 : 1;
    Thread thread = new Thread
                (
                    delegate ()
                    {
                        lock (lockObject)
                        {
                            while (bRunning)
                            {
                                string line = streamReader.ReadLine();
                                if (!string.IsNullOrEmpty(line))
                                {
                                    ParseLine(line.Trim());
                                    MainForm.totalCount++;                                   
                                }
                                if (fileStream.Position < fileStream.Length)
                                {
                                    if (MainForm.totalCount % 10000 is 0)
                                        Thread.Sleep(SleepTime);
                                }
                                else
                                {
                                    Thread.Sleep(SleepTime);
                                }
                            }
                        }
                    }
                );
    worker = thread;
    worker.IsBackground = true;
    worker.Start();
}

이글을 쓰면서
Thread thread = new Thread 이부분의 위치에
worker = thread; 이부분을 합쳐서
Thread thread = worker = new Thread 이렇게 바꾸고
worker = thread; 이걸 삭제하고 테스트를 해봤는데 1초의 차이도 안나네요.

어쨋든 위에서
if (MainForm.totalCount % 10000 is 0) 이부분의 적정값을 못찾았고
if (MainForm.totalCount % 10000 == 0) 이것과의 차이도 모르겠고
실제로 테스트 해도 1초의 차이도 발생하지 않았습니다.
다만 기존의 값은
‘if (MainForm.totalCount % 20 == 0)’ 이거였는데
50, 100, 1000, 늘려가면서 테스트하다가 그냥 10000 넣었습니다…
0을 두어개즘 더 붙혀서 10000과 1000000의 차이가 있을까 했지만 없었습니다.

효과가 없었던 걸로는,

private Regex ㅁㅁ;
private Regex ㅂㅂ;
private Regex ㅇㅇ;

이런걸
private Regex ㅁㅁ, ㅂㅂ, ㅇㅇ;
이렇게 바꾸는건 1초의 차이도 안나는것 같구요.

얼추 1주일가까이 하루에 10시간씩 시간을 붓고
찾아보다가 이 프로그램을 실행하니 작업관리자에서 CPU 코어를
한개만 주구장창 잡아먹고 있는 걸 확인 했습니다.
그리고 프로그램 이름 옆에 (32비트) 라는 글씨를 보고,
다중코어를 사용하게 하는 방법을 찾아보는데, 이건 도통 모르겠고…
릴리즈를(?) 빌드를(?) 64비트로 하는데 성공했습니다.

이건 좀 차이가 많이 나는것 같았습니다.
실제 텍스트 생성시간 1분구간마다 특정한신호를 넣어놔서
프로그램이 구동될때 시간과 해당 신호가 뜨는 시간을 보고 있는데!
평상시엔 실제 1분의 용량을 처리하는데 1분 20~40초 정도 걸렸는데
64비트로 하니 1분의 용량을 처리하는데 58초정도 걸렸습니다!!

밤을새고 아침 9시즘에 뭔가 깨달았습니다!
맨위에 코드인
public void ParseLine(String line) 이부분안에

matches = 차단했습니다Regex.Matches(line);
이런 구문이 2-30개즘 되니 이걸 둘로 나눠서!!

public void ParseLine1(String line)
public void ParseLine2(String line)
같이 나눠서 안에 내용도 나눠서 넣고!

아무튼 그렇게 기존에 있던걸 다
복사해서 두개를 만들고…
하나에는 1을 붙히고 하나에는 2를 붙히고
시행착오를 겪어가면서 그렇게 했는데

와…
와…
아직 미완성이긴한데… 엄청나게 빨라졌습니다.
미친듯이요… 가히 미쳤다고 해도 될만큼 빨라졌네요…
다만이제 그 2개의 내부에서 처리하는 그 양이 한쪽으로 치우쳐 있어서
이거 밸런스를 좀 맞추면 아마 괜찮지 않을가 싶습니다.

이걸 물어보려고 글을 쓰기 시작했는데, 일단 해결이 된거같네요.
글 쓴게 아까워서 일단 올리고!
완성되면 이전코드 이후코드 리뷰 하겠습니다!!
살펴봐주세용!!

코드 수정 상황 및 과정

안녕하세요!!! 며칠동안 끙끙 앓다가 나름 잘 처리를 했습니다.
우선 해결하지 못한 과제(포기한 것)는!!

하나의 파일을 Readline()으로 읽는데, 1초에 약 7-8000줄 정도가 계속 생깁니다. 물론 길어야 10분…
저는 그냥 막연하게, 요즘 컴퓨터들 아무리 똥컴이어도 쿼드코어는 쓰니까 쿼드코어정도를 활용할 수 있는 어떤 그런… 막연한 생각을 하고 검색을 하다보니 스레드를 그만큼 만들어준다거나 현재 사용자의 CPU 코어 갯수를 읽어서 뭐… 엄청 어려워지기 시작하더라구요. 사용자입장에서 마치 코어를 4개 써야지 하고 체크해주면, 알아서 필요한 만큼 쓰는 그런 간단한걸 상상했는데, 어휴…

아무튼 그래서!! 제 자신과 타협을 했습니다.
저렇게 몇분만에 수백만줄이 생기는 경우는 특이한 상황이고, 프로그램 사용자는 그 상황을 사전에 미리 알 수 있는 시스템이어서, 그 상황전에 상황에 맞게 설정을 해놓으면 필요없는 계산은 다 빼버리고 필요한 계산만 하게 했더니 굳이 병렬처리를 하지 않아도 만족할 만큼 빠르더라구요. 그런데 구조상은 사실상 병렬이긴한데, 프로세서는 단일이랄까요…

일단 이전코드!!

        private bool IsFile = false;
        public void Start(string file)
        {
            if (bRunning)
            {
                return;
            }
            if (Starting != null)
            {
                Starting(this, EventArgs.Empty);
            }
            if ((fileStream = OpenFileStream(file)) != null)
            {
                bRunning = true;               

                if (file == Logic.AionLogFileName)
                {
                    IsFile = false;
                    fileStream.Position = fileStream.Length;
                }
                else
                {
                    IsFile = true;
                }
                streamReader = GetStreamReader(fileStream);
                StartWorker();
            }
            if (Started != null)
            {
                Started(this, EventArgs.Empty);
            }
        }
        public void Stop()
        {
            if (!bRunning)
            {
                return;
            }
            else
            {
                bRunning = false;
            }
            if (Stopping != null)
            {
                Stopping(this, EventArgs.Empty);
            }
            if (worker != null)
            {
                worker.Abort();
                worker = null;
            }
            if (streamReader != null)
            {
                streamReader.Close();
                streamReader = null;
            }
            if (fileStream != null)
            {
                fileStream.Close();
                fileStream = null;
            }
            if (Stopped != null)
            {
                Stopped(this, EventArgs.Empty);
            }
            Debug.Write("Log parser stopped.");
        }
        private void StartWorker()
        {
            int SleepTime = (IsFile) ? 1 : 1;
            Thread thread = new Thread
                        (
                            delegate ()
                            {
                                lock (lockObject)
                                {
                                    while (bRunning)
                                    {
                                        string line = streamReader.ReadLine();
                                        if (!string.IsNullOrEmpty(line))
                                        {
                                            ParseLine(line.Trim());
                                            MainForm.totalCount++;
                                            if (MainForm.totalCount > 10000000)
                                            { MainForm.totalCount = 0; }                                            
                                        }
                                        if (fileStream.Position < fileStream.Length)
                                        {
                                            if (MainForm.totalCount % 100 is 0)
                                                Thread.Sleep(SleepTime);
                                        }
                                        else
                                        {
                                            Thread.Sleep(SleepTime);
                                        }
                                    }
                                }
                            }
                        );
            worker = thread;
            worker.IsBackground = true;
            worker.Start();
        } 
    public FileStream OpenFileStream(string file)
        {
            FileStream stream = null;
            try
            {
                stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            }
            catch (Exception e)
            {
                if (e is FileNotFoundException)
                {
                    if (FileNotFound != null)
                    {
                        FileNotFound(this, EventArgs.Empty);
                    }
                }
            }            
            return stream;
        }
        public StreamReader GetStreamReader(FileStream stream)
        {
            if (stream != null)
            {
                BufferedStream bufferedStream = new BufferedStream(stream);
                return new StreamReader(bufferedStream, System.Text.Encoding.Default);
            }
            else
            {
                return null;
            }
        }

이후코드!!

private bool IsFile1 = false;
private bool IsFile2 = false;
public void Start1(string file)
{
    if (bRunning1)
    {
        return;
    }
    if (Starting != null)
    {
        Starting(this, EventArgs.Empty);
    }
    if ((fileStream1 = OpenFileStream1(file)) != null)
    {                
        bRunning1 = true;
        if (file == Logic.AionLogFileName)
        {
            IsFile1 = false;
            fileStream1.Position = fileStream1.Length;
        }
        else
        {
            IsFile1 = true;
        }
        streamReader1 = GetStreamReader1(fileStream1);
        StartWorker1();

    }           
    if (Started != null)
    {
        Started(this, EventArgs.Empty);
    }
}
public void Start2(string file)
{
    if (bRunning2)
    {
        return;
    }
    if (Starting != null)
    {
        Starting(this, EventArgs.Empty);
    }
    if ((fileStream2 = OpenFileStream2(file)) != null)
    {
        bRunning2 = true;
        if (file == Logic.AionLogFileName)
        {
            IsFile2 = false;
            fileStream2.Position = fileStream2.Length;
        }
        else
        {
            IsFile2 = true;
        }
        streamReader2 = GetStreamReader2(fileStream2);
        StartWorker2();
    }
    if (Started != null)
    {
        Started(this, EventArgs.Empty);
    }
}
public void Stop()
{
    if (!bRunning1 && !bRunning2)
    {
        return;
    }
    else
    {
        bRunning1 = false;
        bRunning2 = false;
    }
    if (Stopping != null)
    {
        Stopping(this, EventArgs.Empty);
    }
    if (worker1 != null)
    {
        worker1.Abort();
        worker1 = null;
    }
    if (worker2 != null)
    {
        worker2.Abort();
        worker2 = null;
    }
    if (streamReader1 != null)
    {
        streamReader1.Close();
        streamReader1 = null;
    }
    if (streamReader2 != null)
    {
        streamReader2.Close();
        streamReader2 = null;
    }
    if (fileStream1 != null)
    {
        fileStream1.Close();
        fileStream1 = null;
    }
    if (fileStream2 != null)
    {
        fileStream2.Close();
        fileStream2 = null;
    }
    if (Stopped != null)
    {
        Stopped(this, EventArgs.Empty);
    }
    Debug.Write("Log parser stopped.");
}
private void StartWorker1()
{
    int SleepTime = (IsFile1) ? 1 : 1;
    Thread thread1 = new Thread
                (
                    delegate ()
                    {
                        lock (lockObject1)
                        {
                            while (bRunning1)
                            {
                                string line1 = streamReader1.ReadLine();
                                if (!string.IsNullOrEmpty(line1))
                                {
                                    ParseLine1(line1.Trim());
                                    MainForm.totalCount++;
                                    if (MainForm.totalCount > 10000000)
                                    { MainForm.totalCount = 0; }
                                }
                                if (fileStream1.Position < fileStream1.Length)
                                {
                                    if (MainForm.totalCount % 1000000 == 0) 
                                        Thread.Sleep(SleepTime);
                                }
                                else
                                {
                                    Thread.Sleep(SleepTime);
                                }
                            }
                        }
                    }
                );
    worker1 = thread1;
    worker1.IsBackground = true;
    worker1.Start();
}
private void StartWorker2()
{
    int SleepTime = (IsFile2) ? 1 : 1; //1:1
    Thread thread2 = new Thread
                (
                    delegate ()
                    {
                        lock (lockObject2)
                        {
                            while (bRunning2)
                            {
                                string line2 = streamReader2.ReadLine();
                                if (!string.IsNullOrEmpty(line2))
                                {
                                    ParseLine2(line2.Trim());
                                    MainForm.totalCount++;
                                    if (MainForm.totalCount > 10000000)
                                    { MainForm.totalCount = 0; }
                                }
                                if (fileStream2.Position < fileStream2.Length)
                                {
                                    if (MainForm.totalCount % 1000000 is 0)
                                        Thread.Sleep(SleepTime);
                                }
                                else
                                {
                                    Thread.Sleep(SleepTime);
                                }
                            }
                        }
                    }
                );
    worker2 = thread2;
    worker2.IsBackground = true;
    worker2.Start();
}

public FileStream OpenFileStream1(string file1)
{
    FileStream stream1 = null;
    try
    {
        stream1 = File.Open(file1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    }
    catch (Exception e)
    {
        if (e is FileNotFoundException)
        {
            if (FileNotFound != null)
            {
                FileNotFound(this, EventArgs.Empty);
            }
        }
    }
    return stream1;
}
public FileStream OpenFileStream2(string file2)
{
    FileStream stream2 = null;
    try
    {
        stream2 = File.Open(file2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    }
    catch (Exception e)
    {
        if (e is FileNotFoundException)
        {
            if (FileNotFound != null)
            {
                FileNotFound(this, EventArgs.Empty);
            }
        }
    }
    return stream2;
}
public StreamReader GetStreamReader1(FileStream stream1)
{
    if (stream1 != null)
    {
        BufferedStream bufferedStream1 = new BufferedStream(stream1);
        return new StreamReader(bufferedStream1, System.Text.Encoding.Default);
    }
    else
    {
        return null;
    }
}
public StreamReader GetStreamReader2(FileStream stream2)
{
    if (stream2 != null)
    {
        BufferedStream bufferedStream2 = new BufferedStream(stream2);
        return new StreamReader(bufferedStream2, System.Text.Encoding.Default);
    }
    else
    {
        return null;
    }
}

제가 뭔가 이해를 하고 했다면 훨씬 더 깔끔한 코드가 나왔겠지만… 그냥 거의 헤딩하는 느낌으로… 했습니다!!
ParseLine2(line2.Trim());
원래 이게 정규식을 선언해서 해석하는 구문(?)인데, 이걸 2개로 만들어서 상황에 맞는 버전을 만들었어요.

public void Start1(string file)
메인폼에서 특정상황에 대한 설정을 체크하면, Start1로 시작할지 2로 시작할지 결정을 해놓고 하는걸로 타협을 했습니다. 먹고사는 일도 바쁘고, 시간을 쪼개고 쪼개어 하고, 많은 나이는 아니지만 40대가 훌쩍 넘다보니 뭔가 처음부터 차근차근 배우기도 막연히 불가능하게만 느껴지고요… 1-2년 전만해도 font color=“red” 같은 간단한 html만 알았는데… 이거 만지면서 사이트도 만들고 sql도 해보게 되고… 그랬습니다.

아무튼, 더할나위없이 일단 저는 만족하는데, 좀 전문가님들은 이걸보고 실소를 머금으시겟지요! 그래도 흐뭇한 느낌으로 웃어주시길… 제가 봐도 마치, 2400짜리 길이의 책상을 만들으라고 시켰는데 1200짜리 두개 붙혀놓은 느낌…을 지울수가 없지만… 일단은 잘 되기도 하고 만족합니다!
왜 저렇게 했냐 질타 환영합니다. 질타하시면서 답도 좀 알려주세요. 저거 이게 맞는데 왜 저렇게 했냐 이런식으로… 대부붑 뭐가 뭔지 잘 모르는 코드들 투성입니다ㅠㅠ 작동이 잘되서 안도하는 중…
BufferedStream 이것도 인터넷에서 좋다길래 이케해보고 저케해보고 되길래 냅뒀고… if (MainForm.totalCount % 1000000 is 0) 이부분도 처음엔 20 == 0 이었는데 인터넷 보니까 100 is 0 뭐 이렇게 하길래, 숫자를 높여보니까 빨라서 그냥 냅뒀고… int SleepTime = (IsFile2) ? 1 : 1; 얘도 숫자 낮추니까 빠른데 0:0 하면 그냥 아무것도 안해도 CPU 점유율이 높아지길래 1:1로 했고… 그렇습니다…!

또 조만간에 시시콜콜한 질문을 드려야될거같은데 (내일 오후정도에…?) 여기 질문글을 너무 많이 올리는것같아서 죄송합니다.
또 이글을 보시는 초보님들! 힘내시고 질문 많이 하셔서 많은 개발자들이 찾는 사이트가 (이미 국내에선 최고같지만) 되길 바랍니다! (저보다 더 초보는 단언컨대 없을겁니다!)

1 Like

이런것도 있어요 !

2 Likes

10억행 링크해주신 디모이님과 빅스퀘어님,

그런데…thread 통으로 [critical section]
물론 꼭 필요한 경우에는 lock을 획득하여 처리하는 것이 맞지만,
그것도 크리티컬 섹션을 짧게 가져가면서 부분적으로 성능적 이득을 취할 수 있습니다.
그런데 만약에 그 lock까지 걸려있는 스레드가 invoke를 통해 ui 동기화까지 얽혀있다면…
병렬 컴퓨팅의 성능을 전혀 맛볼 수 없습니다.

라고 댓글 남겨주신 빈센트님!!
(저 글의 뜻을 저 당시에는 진짜… 프랑스어같이 들렸어요…)
지금은 lock 뭔지, 크리티컬 섹션이 뭔지 정도는
오래 안가 까먹겠지만 대충은 알것같습니다.

그리고 병렬 컴퓨팅의 성능!!! 아!! 맛보고 있습니다ㅋ

2 Likes

앱에서 CPU 사용량 측정 - Visual Studio (Windows) | Microsoft Learn

디비그 실행 시 옆에 뜨는 진단 도구를 잘 사용하시면 아래 이미지 처럼 코드의 어떤 부분에서 가장 많은 시간을 소모하는지 확인할 수 있습니다.

image

이 정보를 바탕으로 최적화를 진행하시면 좀 더 쉽게 진행하실 수 있을 겁니다.

2 Likes

이상하게도… 비쥬얼스튜디오에서 디버그 모드를 하면 프로그램이 금새 그냥 꺼져버리는 증상이…ㅠ_ㅠ
제가 24시간 돌리는 어떤 게임과 프로그램이 있는데, 그걸 다 끄고 부팅하면 잘되는데…
아직 방법을 못찾겠습니다. 보안문제라고도 하고… 흑… 조언 감사드려요

1 Like

조금 연식이 된 온라인 게임일 경우
디버거 등을 연결해서 치트엔진처럼 메모리의 변수값을 수정하거나 하는 등의 조작(?)이 가능해서
막는 경우가 간혹 있을거에요.

1 Like

넵 그런거같아요… 비주얼스튜디오 문제라기보다는
맞아요 그런거같아요ㅠ_ㅠ휴…