foreach 구문 처리 속도가 점점 느려집니다.

프로그램 내에서 처음 .csv 파일을 불러왔을 때 바로 사용이 되고, 그 이후에 시간에 따른 조회나 특정 컬럼에 있는 데이터들을 모아주거나 하는 등등 여러 번 사용되는 코드 입니다.

특정 액션을 취할 때 번복해서 사용하는 것은 아닙니다.


가입 초기라 하루에 달 수 있는 댓글 수 제한이 있어 부득이하게 달았던 댓글에 편집을 이용해서 답니다…ㅠㅠ

@nyjin 그러면 하나 여쭙고 싶은 게 있는데

현재는 전역 메서드라고 하나요…? DataTable dt = new DataTable()을 각 메서드 내에서가 아닌 class 내에서 언급을 해주고 그때그때 Clear()를 사용해서 안에 내용을 초기화 해주고 사용을 하고 있었는데,

class에서 언급한 dt 외에 각 메서드에서 DataTable을 생성하여 그 내용을 그대로 class 단위의 DataTable에 넣을 경우에 위와 같은 GC 영향을 안받을 수 있을까요…??

2개의 좋아요

아… 제가 깃허브를 사용을 안해봐서 잘 모르기도 하고, 제 개인 개발을 하고 있지만 회사에 종속된 코드다 보니 그렇게 해도 괜찮을지 모르겠네요 ㅠㅠ…


@dimohy 아 그렇군요…

친절한 답변 및 예시 확인 감사합니다.
dataset을 사용해보려고 생각 중이긴 한데… 안되면 모든 메서드에 대해서 새로운 DataTable을 사용을 하는 방식으로 다시 고민을 해봐야겠네요.

2개의 좋아요

Clear 함수가 DataTableRow만을 삭제할 뿐 Column들은 그대로 두기 때문에 반복 호출할 경우 컬럼 리스트가 누적 증가하여 메모리 누수로 이어질 가능성이 있습니다. (디버깅 해보시거나, 메모리 변화를 확인해 보세요.)
메모리 사용이 일정 수준으로 넘어가게 되면 @Vincent 님 말씀처럼 GC의 호출 빈도가 잦아지고, DataTable에 연결된 Column들은 사용 중인 메모리로 판정되어 삭제되지 않게 되는데요.

DataTable을 매번 초기화해서 사용하는 서비스 시나리오라면, DataTable을 새로 생성해서 사용하시는게 오히려 도움이 될 수 있을 것 같습니다.

6개의 좋아요

.NET Framework 4.7.2에서 간단하게 테스트 코드를 작성한 후 증상을 확인하였습니다.

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var sw = Stopwatch.StartNew();
            DataTable dt = null;
            for (var i = 0; i < 1000; i++)
            {
                dt = MakeTempDataTable(dt);
                Console.WriteLine(sw.Elapsed);
                sw.Restart();
            }
        }

        static DataTable MakeTempDataTable(DataTable dt)
        {
            if (dt is null)
                dt = new DataTable("Temp Table");
            else
            {
                //dt.Rows.Clear();
                //dt.Columns.Clear();

                dt = new DataTable("Temp Table");
            }

            for (var columns = 0; columns < 1000; columns++)
                dt.Columns.Add(new DataColumn($"{columns}1"));

            for (var row = 0; row < 10000; row++)
            {
                var newRow = dt.NewRow();

                for (var columns = 0; columns < 1000; columns++)
                {
                    newRow[columns] = columns;
                }
            }

            return dt;
        }
    }
}

dt.Rows.Clear()dt.Columns.Clear()를 하더라도 DataTable 인스턴스가 가지고 있는 메모리 자원이 GC이후에도 해제되지 않고 계속 증가하는 것을 확인할 수 있었습니다.

DataTable 인스턴스를 새로 만들어서 사용했을 경우 그런 문제가 없었습니다.

결론은, @nyjin님이 제안한 것처럼 그냥 DataTable의 인스턴스를 새로 만들어서 사용해보세요.

8개의 좋아요

코드 구조를 보지 않고 말씀만으로 이슈를 해결하는 것은 분명한 한계가 있기 때문에, 회사 코드라 공유가 어렵다면 모사 코드라도 구성하여 공유해주셔야 이슈도 빠르게 해결되고, 좀더 진전된 대화가 오갈 수 있지 않을까 싶네요.

공유해주신 코드에서 DataTableColumn 추가하는 코드가 실행 된다는 것은 DataTable을 새로 구성할 가능성이 높아 이미 사용중인 DataTable을 폐기하고 새로 구성하여 GC의 메모리 정리 부담을 줄여주는게 좋을 것 같아 dt.Clear() 대신 새로 인스턴스를 구성하는게 좋겠다는 의견을 드렸습니다.

5개의 좋아요

일단 텍스트 파일에서 불러오는 부분을 랜덤값으로 대체하고 다른 부분은 거의 동일하게 테스트 해봤고요…

올려주신 소스에서는 DataTable 정의가 안 보여서, 저는 외부에 선언해놓고 시도해봤습니다.

.NET Framework4.7.2 에서 500개씩 반복적으로 추가했으며, 그때마다 시간을 측정했습니다. (단위: ms)

image

이렇게만 봐서는 Column만 추가하는 건 속도에 영향을 거의 주지 않는 것처럼 보입니다.

소스는 아래와 같이 했습니다.


        DataTable dt = new DataTable();
        private void button1_Click(object sender, EventArgs e)
        {
            DateTime dtBegin = DateTime.Now;

            StringBuilder strTest = new StringBuilder();
            for (int i = 0; i < 500; i++)
            {
                Random rnd = new Random(Guid.NewGuid().GetHashCode());
                if (strTest.Length == 0)
                {
                    strTest.Append("t" + rnd.Next());
                }
                else
                {
                    strTest.Append(",t" + rnd.Next());
                }
            }

            //first line to create header
            string firstLine = strTest.ToString();
            string[] headerLabels = firstLine.Split(',');

            foreach (string headerWord in headerLabels)
            {
                try
                {
                    dt.Columns.Add(new DataColumn(headerWord));
                }
                catch
                {
                    continue;
                }
            }


            textBox1.AppendText($"{dt.Columns.Count} : {(DateTime.Now - dtBegin).TotalMilliseconds} ms" + Environment.NewLine);
        }

5개의 좋아요

아 넵넵 지난번에 달아주셨던 댓글 보고 어느 정도 이해가 되었습니다.

아무래도 메모리가 계속해서 누적되는 문제로 보여 좀 번거롭지만 DataTable을 각각의 메서드에서 언급하여 새로이 데이터를 넣어주려고 합니다.

추후에는 가능하면 코드도 공유가 될 수 있도록 해보겠습니다.

감사합니다.

4개의 좋아요

코드까지 짜서 보여주시는 열정에 감사드립니다.

제가 이 코드를 보고 한번에 이해를 하면 좋겠지만… 그러지 못한 실력에 안타까울 뿐이네요…

2개의 좋아요

소스 보시고 겁먹을 필요 없습니다!! 별 거 없습니다!
이해되는 부분만 이해하시고, 직접 수정해보시는 것이 좋습니다.

그리고 아까 소스에서 첨언을 드리자면, DataTable에 Column을 추가하는 행위만으로는 퍼포먼스에 영향이 거의 없는 것 같다는 결론입니다.
그래서 제가 수정한 부분에서 원인을 찾아야되는데…
텍스트 파일에서 Header string 이걸 불러오는 부분인 것 같습니다.
저는 파일에서 불러오지 않고 자체적으로 랜덤값을 생성해서 그냥 임의의 값을 넣었고,
@lvh0270 님께서는 파일에서 불러왔고요…
이 텍스트 파일의 크기가 점점 커진다거나 하진 않는건가요?
그래서 Split 하는 부분에서 시간이 훨씬 더 많이 소요된다거나 하는 문제는 아닐런지요.

4개의 좋아요

@CODE_REAPER 넵 파일은 수집 된 데이터를 가져온 것이기 때문에 크기는 커지지 않지만,
실행 시킬 때마다 파일에 있는 데이터들을 가져와서 DataTable에 넣은 게 문제가 되는 게 아닐까 생각되기는 합니다.

DataTable 사용 했던 위치가

namespace SQL_CHECK.Winform
{
    public partial class Perfmon : MetroFramework.Forms.MetroForm
    {

        public Perfmon()
        {
         
        }
        DataTable dt = new DataTable();

        //... 나머지 코드
    }
}

이렇게 전역에서 사용할 수 있게 변수를 언급을 해 놓아서 계속해서 초기화가 안되고 데이터가 쌓여서 문제가 되었던 것으로 유추 되네요

2개의 좋아요

흠… 심지어 저는 DataTable을 초기화하지 않고 500개씩 넣는 작업을 반복해서 2만개까지 진행해봐도 퍼포먼스에 영향이 없는 걸로 보였는데…
혹시 저 DataTable을 바인딩하거나 이미 존재하는 DataGridView와 관련된 작업을 하시진 않나요?
그것도 아니면 다른 스레드에서 DataTable에 접근해서 작업한다거나…
원인이 뭔지 엄청 궁금하네요…
@dimohy 님이 말씀하셨듯이 깃허브에 올려주시면 진짜 개비스콘 10개 먹은듯 근심이 사라질 것 같습니다 :rofl:

3개의 좋아요

아… @nyjin 님의 의견이 가장 적절하다는 것은 제 댓글

소스코드를 실행하면 확인할 수 있습니다.

즉 원인은,

DataTable의 dt.Rows.Clear()dt.Columns.Clear() 등으로는 인스턴스가 가지고 있는 자원이 GC이후에도 완전히 해제가 되지 않아 메모리가 계속해서 증가하는 문제가 있습니다.

...
else
{
    dt.Rows.Clear();
    dt.Columns.Clear();

    // dt = new DataTable("Temp Table");
}
...

로 바꾼 후 실행하면 다음과 같이 @lvh0270 님의 문제와 유사한 처리 시간이 증가하는 것을 볼 수 있습니다.

결국에는 @nyjin 님의 의견 처럼 DataTable 자체를 새로 인스턴스를 생성하는 것으로… 문제가 해결된다는 것이죠.

image

4개의 좋아요

@dimohy @Vincent @nyjin @CODE_REAPER @aroooong

많은 관심과 가르침 감사드립니다.

결국에는 메서드 마다 DataTable을 새로 언급을 해주면서 그냥 계속 데이터를 넣어주는 걸로 다 변경했습니다.
지금은 속도가 잘 나옵니다 핳핳…

다시 한번 감사드립니다.

8개의 좋아요

이게 코드를 얼마나 올려야 될 지를 몰라서 ㅎㅎ…

한번 올려보겠습니다.

어차피 지금은 다 다시 새로 수정하고 해서 지우는 데이터라 상관없을 거 같네요…
다만 제가 깃허브를 한번도 사용을 안해봐서 방법을 좀 찾아보겠습니다

일단은 맨처음에 Vincent님이 말씀하신 것 처럼 DataTable이 반복해서 사용하므로써 메모리가 점점 차올라서 느려졌던 것으로 보입니다.

깃허브 겁나 어렵네요 ㅋㅋㅋㅋㅋㅋㅋ 휴…

어떻게 저쩧게 올렸습니다 ㅠㅠ…

6개의 좋아요

이거 아무리 생각해도 이상해서

검색도 하고 테스트도 돌려봤는데요.

Columns.Clear() 나 Rows.Clear() 와는 상관이 없었고

함정은 var newRow = dt.NewRow(); 에 있었네요.

DataTable? MakeTempDataTable(DataTable? dt)
{
    if (dt is null)
    {
        dt = new DataTable("Temp Table");
    }
    else
    {
        dt.Rows.Clear();        
        dt.Columns.Clear();
    }

    for (var columns = 0; columns < 1000; columns++)
        dt.Columns.Add(new DataColumn());


    for (var row = 0; row < 10000; row++)
    {
        var newRow = dt.NewRow();

        for (var columns = 0; columns < 1000; columns++)
        {
            newRow[columns] = columns;
        }

        dt.Rows.Add(newRow); // 요거 필수.
    }

    return dt;
}

진짜인지는 모르겠지만
dt.Rows.Add(newRow); 이거 있고 없고 차이를 설명한 글

10개의 좋아요

네. 그렇네요. DataTable을 오랫동안 사용하지 않고… 콘솔에서 테스트했더니 실수로 누락했네요. 확인 감사드립니다.

2개의 좋아요

질문에 대한 답변 아닙니다. ㅠ

와, 이런 건강하고 조직적인 커뮤니티라니…
세상 개발자들이 다 알아야 할텐데…

4개의 좋아요

@dimohy 넵. 저도, “이럴리 없는데…??” 라는 쎄…한 느낌이 나서
뭐가 영향을 미치는 지 알아보려고 이것저것 뒤졌네욤ㅋㅅㅋ

dt.Rows.Add(newRow); 요 한 줄 안 쓴 것만으로 메모리 누수가 발생할 수 있다는 점은 꽤 놀라웠슴다.
(반대로 이 한 줄로 속도와 메모리 모두 안정적으로 바뀐다는 게 놀랍기도…)

ORM 이 대세가 되면서 DataSet DataTable 을 사용하지 않은 지 꽤 되어서 그런가
한창 쓸 때에는 당연하게 받아들였는데, 이거 생각보다 심각한 문제이지 않나 싶어요.

사용성을 생각해보면 NewRow() 호출 타이밍 보다 ‘dt.Rows.Add(newRow);’ 타이밍에
DataTable 의 스키마를 할당하는 처리가 들어가는 게 그 편이 더 안전하지 않을까 하는 생각이 듭니다.

여튼 간만에 닷넷 소스 찾아다녔네용 ㅋㅅㅋ

5개의 좋아요

어… 제가 지금 이 글이랑 링크 걸어주신 부분까지 다 읽어봤는데

난독증인건지… 잘 이해가 안되는데 newRow가 있으면 메모리 누수가 없고
newRow가 없으면 메모리 누수가 있다는건가요…?

아니면 그 반대라는 이야기인가요?

2개의 좋아요

NewRow()로 생성한 DataRow 객체를

Add()로 DataTable에 넣어주거나,
Remove() 로 제거하지 않으면
그대로 메모리에 남는다.(둘 중 하나는 꼭 해야 한다)
요걸로 이해했어요 저는,
혹시 다른분들 말씀하신게 이제 맞을까요?

1개의 좋아요