Tuple<long,long,string>
간단한 “사이”검색을 수행하는 무작위로 생성 된 500000 개의 개체 목록이 있습니다 .
var data = new List<Tuple<long,long,string>>(500000);
...
var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x);
무작위 배열을 생성하고 100 개의 무작위로 생성 된 값에 대한 검색을 실행 x
하면 약 4 초 안에 검색이 완료됩니다. 그러나 정렬이 검색 하는 데있어 놀라운 점을 알고 있었지만, 100 개의 검색을 실행하기 전에 먼저 데이터를 정렬 Item1
한 다음 Item2
마지막으로 정렬하기로 결정했습니다 Item3
. 분기 예측으로 인해 정렬 된 버전이 약간 더 빨리 수행 될 것으로 예상했습니다. 내 생각은 일단 우리가 Item1 == x
더 이상 모든 검사가 t.Item1 <= x
지점을 “취득 없음”으로 정확하게 예측하여 검색. 놀랍게도 정렬 된 배열에서 검색하는 데 두 배가 걸렸습니다 !
실험을 실행 한 순서를 바꾸고 난수 생성기에 다른 시드를 사용했지만 그 효과는 동일합니다. 정렬되지 않은 배열의 검색은 같은 배열의 검색보다 거의 두 배 빠르지 만 분류!
누구든지이 이상한 효과에 대해 잘 설명하고 있습니까? 내 테스트의 소스 코드는 다음과 같습니다. .NET 4.0을 사용하고 있습니다.
private const int TotalCount = 500000;
private const int TotalQueries = 100;
private static long NextLong(Random r) {
var data = new byte[8];
r.NextBytes(data);
return BitConverter.ToInt64(data, 0);
}
private class TupleComparer : IComparer<Tuple<long,long,string>> {
public int Compare(Tuple<long,long,string> x, Tuple<long,long,string> y) {
var res = x.Item1.CompareTo(y.Item1);
if (res != 0) return res;
res = x.Item2.CompareTo(y.Item2);
return (res != 0) ? res : String.CompareOrdinal(x.Item3, y.Item3);
}
}
static void Test(bool doSort) {
var data = new List<Tuple<long,long,string>>(TotalCount);
var random = new Random(1000000007);
var sw = new Stopwatch();
sw.Start();
for (var i = 0 ; i != TotalCount ; i++) {
var a = NextLong(random);
var b = NextLong(random);
if (a > b) {
var tmp = a;
a = b;
b = tmp;
}
var s = string.Format("{0}-{1}", a, b);
data.Add(Tuple.Create(a, b, s));
}
sw.Stop();
if (doSort) {
data.Sort(new TupleComparer());
}
Console.WriteLine("Populated in {0}", sw.Elapsed);
sw.Reset();
var total = 0L;
sw.Start();
for (var i = 0 ; i != TotalQueries ; i++) {
var x = NextLong(random);
var cnt = data.Count(t => t.Item1 <= x && t.Item2 >= x);
total += cnt;
}
sw.Stop();
Console.WriteLine("Found {0} matches in {1} ({2})", total, sw.Elapsed, doSort ? "Sorted" : "Unsorted");
}
static void Main() {
Test(false);
Test(true);
Test(false);
Test(true);
}
Populated in 00:00:01.3176257
Found 15614281 matches in 00:00:04.2463478 (Unsorted)
Populated in 00:00:01.3345087
Found 15614281 matches in 00:00:08.5393730 (Sorted)
Populated in 00:00:01.3665681
Found 15614281 matches in 00:00:04.1796578 (Unsorted)
Populated in 00:00:01.3326378
Found 15614281 matches in 00:00:08.6027886 (Sorted)
답변
정렬되지 않은 목록을 사용하는 경우 모든 튜플은 메모리 순서 로 액세스됩니다 . RAM에 연속적으로 할당되었습니다. CPU는 다음 캐시 라인을 추론 적으로 요청할 수 있으므로 필요할 때 항상 존재하기 때문에 메모리에 순차적으로 액세스하는 것을 좋아합니다.
정렬 키가 무작위로 생성되므로 목록을 정렬 할 때 임의 순서 로 나열합니다 . 이것은 튜플 멤버에 대한 메모리 액세스가 예측할 수 없음을 의미합니다. CPU는 메모리를 프리 페치 할 수 없으며 튜플에 대한 거의 모든 액세스는 캐시 누락입니다.
이것은 GC 메모리 관리 의 특정 이점에 대한 좋은 예입니다. 함께 할당되고 함께 사용되는 데이터 구조는 매우 훌륭하게 수행됩니다. 그들은 큰 참조 지역을 가지고 있습니다 .
이 경우 캐시 미스의 페널티 가 저장된 브랜치 예측 페널티보다 큽니다 .
struct
-tuple로 전환 해보 십시오. 튜플 멤버에 액세스하기 위해 런타임에 포인터 역 참조가 필요하지 않기 때문에 성능이 복원됩니다.
크리스 싱클레어 (Chris Sinclair)는 논평에서 “약 10,000 이하의 TotalCount의 경우 정렬 된 버전이 더 빠르게 수행된다 “고 언급했다. 작은 목록 이 CPU 캐시에 완전히 맞기 때문 입니다. 메모리 액세스는 예측할 수 없지만 대상은 항상 캐시에 있습니다. 캐시에서로드조차도 약간의주기가 걸리기 때문에 여전히 작은 페널티가 있다고 생각합니다. 그러나 CPU가 여러 개의 미해결로드를 저글링 하여 처리량을 증가시킬 수 있기 때문에 문제가되지 않는 것 같습니다 . CPU가 메모리 대기에 도달 할 때마다 명령 스트림에서 속도를 높여서 가능한 한 많은 메모리 작업을 대기시킵니다. 이 기술은 대기 시간을 숨기는 데 사용됩니다.
이러한 종류의 동작은 최신 CPU의 성능을 예측하기가 얼마나 어려운지를 보여줍니다. 순차 메모리에서 랜덤 메모리 액세스로 갈 때 우리가 단지 2 배 더 느리다 는 사실은 메모리 대기 시간을 숨기기 위해 커버 아래에서 진행되는 양을 알려줍니다. 메모리 액세스는 50-200주기 동안 CPU를 정지시킬 수 있습니다. 이 숫자를 고려하면 임의의 메모리 액세스를 도입 할 때 프로그램이 10 배 이상 느려질 것으로 예상 할 수 있습니다.
답변
LINQ는 목록이 정렬되어 있는지 여부를 모릅니다.
술어 매개 변수가있는 Count는 모든 IEnumerables의 확장 방법이므로 효율적인 임의 액세스로 컬렉션에서 실행되는지조차 알 수 없다고 생각합니다. 따라서 단순히 모든 요소를 확인하고 Usr 은 왜 성능이 저하되었는지 설명했습니다.
정렬 된 배열 (예 : 이진 검색)의 성능 이점을 활용하려면 코딩을 조금 더해야합니다.