Microsoft MVP성태의 닷넷 이야기
string.Join()과 Enumerable.Aggregate()의 차이가 궁금합니다. [링크 복사], [링크+제목 복사]
조회: 10536
글쓴 사람
이성환 (vactorman at naver.com)
홈페이지
첨부 파일



List 요소의 특정 멤버들을 한 문자열로 나열하기 위해

string.Join()과 Aggregate() 를 이용해 구분자를 삽입하여 문자열로 연결하는 테스트를 하다가

두 메서드의 수행 속도가 예상한 것과 다르다는 것을 깨달았습니다.


그리고 또 LINQPad와 실제 vs2012에서 작성한 Console 프로그램의 성능 차이가 확인돼서 이렇게 질문 올립니다.


말로 설명하면 더 길어질거 같아 테스트한 코드를 먼저 보면

public class Person
{
    public string Name { get; set; }
}

이런 클래스가 있고




var list = new List<Person>();

for (var i = 0; i < 1000; i++)
{
    var newName = new Person { Name = i.ToString() };
    list.Add(newName);
}

var start1 = Stopwatch.GetTimestamp();

var result1 = list.Select(person => person.Name).Aggregate((current, next) => current + "," + next);

Console.WriteLine(result1);

var elapsed1 = (Stopwatch.GetTimestamp() - start1) / (double)Stopwatch.Frequency;
Console.WriteLine();
Console.WriteLine("Aggregate : " + elapsed1);

var start2 = Stopwatch.GetTimestamp();

var result2 = string.Join(",", list.Select(person => person.Name));
Console.WriteLine(result2);

var elapsed2 = (Stopwatch.GetTimestamp() - start2) / (double)Stopwatch.Frequency;

Console.WriteLine();

Console.WriteLine("Join : " + elapsed2);


이렇게 리스트의 요소 중 Name 멤버들을 구분자로 넣어서 하나의 문자열을 만드는 과정을 구성했습니다.

그리고 각 문자열 생성 작업 시간을 측정해 보았는데

string.Join() 과 Aggregate() 의 수행 시간이 들쑥날쑥 합니다.

ReBuild 할 때마다 어떨 때는 Join()이 빨랐다가 또 어떨때는 Aggregate()가 빨랐다가

근소한 차이로 두 메서드의 결과가 엎치락 뒤치락 하는 등

일정한 결과가 나오지 않았는데요.
(List 개수 1000개 정도에서는 그랬습니다.)

생성된 문자열 객체를 사용하지 않은 상태(Console.WriteLine으로 문자열을 찍지 않은 상태)에서 시간측정을 하면

Join()이 10~20배 정도로 빠릅니다.

그리고 List의 크기가 커지면 (10000개 이상으로 늘리면) 그 때부터는 확실히 Join()이 빨라집니다.


궁금한 점은

1. Console.WriteLine()으로 생성된 문자열을 찍지 않은 상태라면 Aggregate()가 Join()보다 더 빨라야 하는 게 아닌가요?

Console.WriteLine()으로 생성된 문자열을 찍지 않은 상태라면

Aggregate()의 경우 실제 요소에 접근하지 않았으므로 메서드 완료 속도가 더 빨라야 하는게 아닌가

하는 것이 처음 생각이었는데 결과가 정반대여서 의문이 생겼습니다.

어차피 list.Select(person => person.Name) 이 구문까지는 Join()과 Aggregate()가 모두 같은 조건이라

Select() 호출까지의 결과는 크게 다르지 않았을 것인데

정작 Aggregate()의 시간이 더 많이 걸린다는 것은 실제 요소 접근 구문이 없어도 누산 작업을 수행했고

누산 시 모든 요소에 접근하는 게 아닌가 하는 생각이 듭니다.


그렇다쳐도 구현내용을 겉핥기로만 봤을 때

Join()은 StringBuilder를 이용해 while을 돌면서 Append 한 것을 리턴하지만

Aggregate()는 while을 돌면서 그냥 func 를 수행한 결과를 이전 요소에 할당하는 정도로 끝나기 때문에 StringBuilder 보다 더 빨라야 한다고 생각했는데
(타입이 이미 string이므로 메모리 상의 손해는 있을 지언정 속도는 더 빨라야 한다고 생각했습니다.)

왜 이런 결과가 나왔는지 궁금합니다.


그런데 사실 이 테스트는 vs2012보다 LINQPad에서 먼저 수행했습니다.

첨부한 파일처럼 수행했었는데요.

LINQPad에서는 일정하게 Join()이 Aggregate보다 훨씬 빠릅니다. 그리고 리스트의 크기를 크게 할수록 속도차이가 확연해집니다.
(이 부분은 Console과 같습니다.)

헌데 가장 이해가 안 되는 부분은 생성된 문자열을 Dump()를 이용해 모두 결과창에 표시했을 때 입니다.

vs2012의 Console 테스트에서는 생성된 문자열을 Console에 찍었을 경우

Release나 Debug 모두 속도차이가 그리 크지 않게 나왔습니다.
(물론 이 경우에도 리스트의 크기가 클 수록 Join()의 속도가 더 빠릅니다만 LINQPad만큼의 차이는 나지 않았습니다.)


그런데 LINQPad에서는 결과를 모두 표시해도 Join()이 훨씬 빠르게 수행되는 것을 확인했습니다.

혹시나 해서 JITter의 영향이 있을까봐 테스트 구문 수행 전에

var tempList =Enumerable.Range(0, 100);
tempList.Aggregate((current, next) => current + next);
string.Join(",", tempList);

list.Select(person => person.Name);

뭐 이런 코드를 먼저 호출하고 시작하는 뻘짓도 해봤지만

별 차이가 없었습니다.



이렇게 차이가 나는 것도 의문이고

그렇다면 실행 결과는 LINQPad 보다 Console 프로그램의 결과를 더 신뢰해야하는 건지 의문이 듭니다.



보통 프로토타이핑이나 테스트 코드를 만드는 작업은 LINQPad를 주로 사용하는데

이렇게 테스트를 하고 난 후부터는 구문상의 오류 따위가 아니라 이런 퍼포먼스에 관련된 작업이라면

반드시 Visual Studio로 테스트를 해봐야겠다는 생각이 듭니다.

아니면 뭔가 제가 테스트를 잘못한 걸까요?


도움 부탁드립니다.










[최초 등록일: ]
[최종 수정일: 2/28/2014]


비밀번호

댓글 작성자
 



2014-03-01 03시10분
LINQ의 표준 연산자가 모두 지연 처리를 하는 것은 아닙니다. IEnumerable 계열을 반환하지 않는 단일값 반환 연산자들은 그 즉시 처리되는 것이 일반적입니다. Aggregate의 경우 반환값이 TSource 단일값인데 이는 LINQ식이 평가되면서 곧바로 실행됩니다.

성능 차이에 대해서는 제가 일단 관심이 없어서 넘어가겠습니다. 어차피 .NET Reflector로 조사하면 뭔가 환경적인 요인이나 테스트 코드 상의 차이가 있을 텐데... 이 부분은 이성환님이 좀더 ^^ 살펴보시고 한편의 멋진 글을 써주시길 기대하겠습니다. (나중에 이 질문에 흥미가 가면 혹시 쓸지도 모르겠습니다. ^^)
정성태
2014-03-01 05시44분
[이성환] 답변 감사드립니다.

Aggregate()가 지연처리 하지 않는 것은 잘 몰랐던 부분입니다. 결과가 지연처리가 아닌 것처럼 나와서 그렇게만 추측한 정도였죠
사실 LINQ 표준 연산자가 단일값을 반환할 때 지연처리 한다는 사실을 몰랐습니다. 하나 또 배웠습니다.

성능 상의 차이 역시 사실
제가 그 정도로 깊게 조사해 볼 수 있는 능력이 없어서 리플렉터로 구현된 코드를 보고 추측하는 정도라
더 진전이 없어 질문을 드린 건데요. ;ㅅ; 공부가 더 필요한 걸 절실히 느낍니다.

여튼 답변 주셔서 감사합니다. .(__).

[guest]

... 31  32  33  34  35  36  37  38  [39]  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
4878heyh...8/25/201710045프로세스의 프로세스 찾기(?) [2]
4877강준8/24/20179272SQLite journal_mode=wal 관련하여 질문드립니다. [1]
4876heyh...8/23/20179485프로세스 초기화하기 [1]
4874ho8/22/20179579파일 확장자명을 이용해 파일의 실행 프로그램의 전체 경로를 얻어 올 수 있을까요? [1]
4875ho8/23/201710551    답변글 [답변]: 파일 확장자명을 이용해 파일의 실행 프로그램의 전체 경로를 얻어 올 수 있을까요? [1]
4873kmi8/21/201711354전역 변수를 쓰지 않고 여러 군데에서 같은 변수를 공용하는 방법이 궁금합니다. [4]
4872abcd8/18/201710027프로세스를 초기화 하는 명령어도 있나요? [1]
4871kmi8/17/201711346메모리 부족으로 종료되는 현상의 여러가지 이유가 무엇인지 궁금합니다. [2]
4870heyh...8/14/201710146프로그램 실행시 중복일 때 버튼 색깔 원래대로 돌리기 [1]
4868kmi8/4/20179462string[] 에 Reverse 적용방법 질문해봅니다 [3]
4867heyh...8/4/20178941EventHandler에 관한 [1]
486610년차8/3/20179380dsoframer axframer open시 기존 오픈되어있는 엑셀을 먹어버리는 현상 [1]
4865heyh...7/31/201710950클릭원스로 배포 한 프로젝트가 끝났는지 알 수 있는 방법 [8]
4864초보자7/28/20179948DllIImport질문 드립니다. [1]
4863다연아빠7/23/201710166전역 예외처리에 대해 질문있습니다. [3]
4861라르크7/17/201715421window form 예제 따라하는 중인데 12.3 서비스 응용 프로그램에서 진행이 안됩니다. [3]파일 다운로드1
4859heyh...7/10/201710075다른 환경에서 실행하기 [1]
4858heyh...7/10/20179918Clickonce update에 관한질문입니다. [1]
4857heyh...7/7/201710565제가 여태까지 작성한 보고서입니다. [2]파일 다운로드1
4856heyh...7/6/20179278성태님 다른질문입니다. [4]
4855JP7/6/20179957Dispose 패턴 구현시 Finalize 재정의에 대한 질문드립니다. [2]
4854heyh...7/6/20179405
4853heyh...7/5/20179513성태님이 작성한대로 해봤습니다. [1]파일 다운로드1
4852김레오7/4/201711671서드파티 dll 디버깅에 대해 질문드립니다. [2]
4851김현준7/3/201710787Datagridview VirtualMode 시 GC가 계속 호출되는 현상이 이해가 안갑니다. [2]
4850heyh...7/3/201710539성태님 밑에 질문드렸던 오류입니다. [1]파일 다운로드1
... 31  32  33  34  35  36  37  38  [39]  40  41  42  43  44  45  ...