LINQ 메서드의 런타임 복잡성 (Big-O)에 대해 어떤 보장이 있습니까?


120

저는 최근에 LINQ를 꽤 많이 사용하기 시작했으며 LINQ 메서드에 대한 런타임 복잡성에 대한 언급을 실제로 보지 못했습니다. 분명히 여기에는 많은 요소가 작용하므로 일반 IEnumerableLINQ-to-Objects 공급자에 대한 논의를 제한하겠습니다 . 또한 Func선택자 / 변이 자 등으로 전달 된 모든 것이 저렴한 O (1) 연산 이라고 가정 해 봅시다 .

그것은 분명한 것 같다 모든 싱글 패스 (single-pass) 작업 (즉 Select, Where, Count, Take/Skip,Any/All 그들이 한 번만 순서를 걸어야하기 때문에, 등), O (n)이 될 것입니다; 이것조차도 게으름의 대상입니다.

더 복잡한 작업의 경우 상황이 더 어둡습니다. 세트 같은 사업자 ( Union, Distinct, Except, 등)를 사용하여 작업 GetHashCode기본적으로 (AFAIK), 그들이 일반적으로,뿐만 아니라 이러한 작업의 O (N)를 만들고, 내부적으로 해시 테이블을 사용하는 가정하는 것이 합리적 것 때문에. 를 사용하는 버전은 IEqualityComparer어떻습니까?

OrderBy정렬이 필요하므로 O (n log n)을보고있을 가능성이 높습니다. 이미 정렬 된 경우 어떻게합니까? OrderBy().ThenBy()둘 다 동일한 키를 말하고 제공하면 어떨까요?

정렬 또는 해싱을 사용하여 GroupBy(및 Join)을 볼 수 있습니다 . 무엇 이니?

Contains은 O (n) List이지만 O (1) HashSet은-LINQ가 기본 컨테이너를 확인하여 속도를 높일 수 있는지 확인합니까?

그리고 진짜 질문은-지금까지 저는 그 수술이 성능이 좋다는 믿음으로 받아 들였습니다. 그러나 그것에 은행을 둘 수 있습니까? 예를 들어 STL 컨테이너는 모든 작업의 ​​복잡성을 명확하게 지정합니다. .NET 라이브러리 사양에서 LINQ 성능에 대한 유사한 보장이 있습니까?

더 많은 질문 (코멘트에 대한 응답) :
오버 헤드에 대해 실제로 생각하지는 않았지만 간단한 Linq-to-Objects에 대해 그다지 많지 않을 것이라고 예상했습니다. CodingHorror 게시물은 Linq-to-SQL에 대해 이야기하고 있습니다. 여기서 쿼리를 구문 분석하고 SQL을 만들면 비용이 추가되는 것을 이해할 수 있습니다. Objects 공급자에게도 비슷한 비용이 있습니까? 그렇다면 선언적 또는 기능적 구문을 사용하는 경우 다른가요?


귀하의 질문에 실제로 대답 할 수는 없지만 일반적으로 성능의 큰 부분이 핵심 기능에 비해 "오버 헤드"가 될 것이라고 언급하고 싶습니다. 물론 이것은 매우 큰 데이터 세트 (> 10,000 개 항목)가있는 경우가 아니므로 어떤 경우에 알고 싶은지 궁금합니다.
Henri

2
Re : "선언적 또는 기능적 구문을 사용하는 경우 다른가요?" -컴파일러는 선언적 구문을 기능적 구문으로 변환하여 동일하게 만듭니다.
John Rasch

"STL 컨테이너는 모든 작업의 ​​복잡성을 명확하게 지정합니다.".NET 컨테이너는 또한 모든 작업의 ​​복잡성을 명확하게 지정합니다. Linq 확장은 STL 컨테이너가 아니라 STL 알고리즘과 유사합니다. STL 알고리즘을 STL 컨테이너에 적용 할 때와 마찬가지로 Linq 확장의 복잡성과 .NET 컨테이너 작업의 복잡성을 결합하여 결과 복잡성을 적절하게 분석해야합니다. 여기에는 Aaronaught의 답변에서 언급했듯이 템플릿 전문화에 대한 설명이 포함됩니다.
Timbo

근본적인 질문은 개발자가 자신의 코드가 성능을 발휘하기 위해 문서화되지 않은 동작에 의존해야한다는 점을 감안할 때 IList <T> 최적화의 유용성이 제한적일 것이라고 Microsoft가 더 염려하지 않은 이유입니다.
Edward Brey

결과 집합 목록에 대한 AsParallel (); ~ O (1) <O (n)
지연 시간

답변:


121

보장은 거의 없지만 몇 가지 최적화가 있습니다.

  • 같은 인덱스 액세스를 사용 확장 방법, ElementAt, Skip, Last또는 LastOrDefault, 여부 기본 타입의 구현을 확인합니다 IList<T>, 그래서 당신은 O (N)의 O (1) 접근 대신 얻을.

  • Count위한 검사 방법 ICollection의 구현되므로이 조작인지 O (1)이 아닌 O (N).

  • Distinct,, GroupBy Join그리고 집합 집계 방법 ( Union, IntersectExcept)도 해싱을 사용하므로 O (N²) 대신 O (N)에 가까워 야합니다.

  • Contains을 검사 ICollection구현은 그렇게 수도 기본 모음 등으로, 또한, O (1)의 경우 O (1) 일 수 HashSet<T>있지만, 인 실제 데이터 구조에 의존하지 않을 수 있습니다. 해시 세트는 Contains메서드를 재정의하므로 O (1)입니다.

  • OrderBy 메서드는 안정적인 퀵 정렬을 사용하므로 O (N log N) 평균 케이스입니다.

나는 그것이 모든 내장 확장 방법은 아니지만 대부분을 포함한다고 생각합니다. 실제로 성능 보장은 거의 없습니다. Linq 자체는 효율적인 데이터 구조를 활용하려고 시도하지만 잠재적으로 비효율적 인 코드를 작성하는 것은 자유 패스가 아닙니다.


방법에 대한 IEqualityComparer과부하?
tzaman

@tzaman : 그들에 대해? 정말 비효율적 인 custom을 사용하지 않는 한 IEqualityComparer점근 적 복잡성에 영향을 미칠 이유가 없습니다.
Aaronaught

1
아, 맞다. 나는 뿐만 아니라 EqualityComparer구현 GetHashCode을 실현하지 못했습니다 Equals; 물론 그것은 완벽하게 이해됩니다.
tzaman

2
@imgen : 루프 조인은 관련없는 집합에 대해 O (N²)로 일반화되는 O (N * M)입니다. Linq는 O (N + M) 인 해시 조인을 사용하여 O (N)으로 일반화합니다. 그것은 반 정도 괜찮은 해시 함수를 가정하지만 .NET에서 엉망이 되기는 어렵습니다.
Aaronaught 2014 년

1
입니다 Orderby().ThenBy()여전히 N logN또는 인 (N logN) ^2이나 뭐 그런?
M.kazem Akhgary 2015

10

나는 그렇게 오래 알고 지낸 .Count()수익을 .Count열거가 인 경우IList .

하지만 설정 작업의 실행 시간 복잡도에 대한 지친 조금 언제나 : .Intersect(), .Except(), .Union().

다음은 .Intersect()(내 주석 )에 대한 디 컴파일 된 BCL (.NET 4.0 / 4.5) 구현입니다 .

private static IEnumerable<TSource> IntersectIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)                    // O(M)
    set.Add(source);                                    // O(1)

  foreach (TSource source in first)                     // O(N)
  {
    if (set.Remove(source))                             // O(1)
      yield return source;
  }
}

결론 :

  • 성능은 O (M + N)
  • 컬렉션이 이미 설정된 경우 구현 이점을 얻지 못합니다 . (사용 된 것과 일치해야하기 때문에 반드시 간단하지 않을 수 있습니다 .)IEqualityComparer<T>

완전성을 위해 다음은 .Union().Except() .

스포일러 경고 : 그들 역시 O (N + M) 복잡성을 가지고 있습니다.

private static IEnumerable<TSource> UnionIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
  foreach (TSource source in second)
  {
    if (set.Add(source))
      yield return source;
  }
}


private static IEnumerable<TSource> ExceptIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)
    set.Add(source);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
}

8

실제로 활용할 수있는 것은 Enumerable 메서드가 일반적인 경우에 잘 작성되고 순진한 알고리즘을 사용하지 않는다는 것입니다. 실제로 사용중인 알고리즘을 설명하는 타사 자료 (블로그 등)가있을 수 있지만 STL 알고리즘이라는 의미에서 공식적이거나 보장되지는 않습니다.

다음은 Enumerable.CountSystem.Core 의 반영된 소스 코드 (ILSpy 제공)입니다 .

// System.Linq.Enumerable
public static int Count<TSource>(this IEnumerable<TSource> source)
{
    checked
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        ICollection<TSource> collection = source as ICollection<TSource>;
        if (collection != null)
        {
            return collection.Count;
        }
        ICollection collection2 = source as ICollection;
        if (collection2 != null)
        {
            return collection2.Count;
        }
        int num = 0;
        using (IEnumerator<TSource> enumerator = source.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                num++;
            }
        }
        return num;
    }
}

보시다시피 모든 요소를 ​​단순히 열거하는 순진한 해결책을 피하기 위해 노력합니다.


IEnnumerable 인 경우 Count ()를 얻기 위해 전체 개체를 반복하는 것은 나에게 매우 순진 해 보입니다 ...
Zonko

4
@Zonko : 나는 당신의 요점을 이해하지 못합니다. 나는 Enumerable.Count명백한 대안이 없으면 반복되지 않음 을 보여주기 위해 내 대답을 수정했습니다 . 어떻게 덜 순진하게 만들었 을까요?
Marcelo Cantos 2011

네, 방법은 소스가 주어지면 가장 효율적인 방법으로 구현됩니다. 그러나 가장 효율적인 방법은 때때로 순진한 알고리즘이며, linq를 사용할 때는 호출의 실제 복잡성을 숨기므로주의해야합니다. 조작중인 객체의 기본 구조에 익숙하지 않은 경우 필요에 따라 잘못된 방법을 쉽게 사용할 수 있습니다.
Zonko 2011

@MarceloCantos 왜 배열이 처리되지 않습니까? ElementAtOrDefault 메서드에서도 동일합니다. referencesource.microsoft.com/#System.Core/System/Linq/…
Freshblood

@Freshblood 그들은 있습니다. (배열은 ICollection을 구현합니다.)하지만 ElementAtOrDefault에 대해서는 모릅니다. 배열도 ICollection <T>를 구현한다고 생각하지만 요즘에는 .Net이 상당히 녹슬 었습니다.
Marcelo Cantos 2015

3

방금 리플렉터를 부수고 Contains호출 될 때 기본 유형을 확인합니다 .

public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
    ICollection<TSource> is2 = source as ICollection<TSource>;
    if (is2 != null)
    {
        return is2.Contains(value);
    }
    return source.Contains<TSource>(value, null);
}

3

정답은 "상황에 따라 다름"입니다. 기본 IEnumerable이 어떤 유형인지에 따라 다릅니다. ICollection 또는 IList를 구현하는 컬렉션과 같은 일부 컬렉션에는 특수 코드 경로가 사용된다는 것을 알고 있지만 실제 구현은 특별한 작업을 보장하지 않습니다. 예를 들어, ElementAt ()에는 Count ()와 마찬가지로 인덱스 가능한 컬렉션에 대한 특별한 경우가 있다는 것을 알고 있습니다. 그러나 일반적으로 최악의 경우 O (n) 성능을 가정해야합니다.

일반적으로 원하는 성능 보장을 찾을 수 없다고 생각하지만 linq 연산자로 특정 성능 문제가 발생하면 항상 특정 컬렉션에 대해 다시 구현할 수 있습니다. 또한 Linq를 Objects로 확장하여 이러한 종류의 성능 보장을 추가하는 많은 블로그와 확장 성 프로젝트가 있습니다. 더 많은 성능 이점을 위해 연산자 집합을 확장하고 추가하는 Indexed LINQ 를 확인하십시오 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.