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]