이 캐싱 전략에 어떤 데이터 구조를 사용해야합니까?


11

.NET 4.0 응용 프로그램을 작성 중입니다.이 응용 프로그램은 두 배의 두 배에 대해 값 비싼 계산을 수행하여 배를 반환합니다. 이 계산은 수천 개의 항목 중 하나에 대해 수행 됩니다 . 이러한 계산은 Task스레드 풀 스레드 에서 수행됩니다 .

일부 예비 테스트에서 동일한 계산이 반복적으로 수행되는 것으로 나타 났으므로 n 개의 결과 를 캐시하고 싶습니다 . 캐시가 가득 차면 가장 자주 사용하지 않는 항목 을 버리고 싶습니다 . ( 편집 : 캐시가 가득 차서 결과를 새로 계산 된 결과로 바꾸면 다음에 새로운 결과가 계산 될 때 가장 자주 사용되지 않고 즉시 대체되기 때문에 나는 종종 이해가되지 않는다는 것을 깨달았습니다 . 캐시에 추가)

이것을 구현하기 위해 입력과 캐시 된 결과를 저장하기 위해 (두 개의 입력 이중 값을 저장하는 미니 클래스가있는 Dictionary<Input, double>곳) 을 사용하려고 생각했습니다 Input. 그러나 결과가 마지막으로 사용 된 시점을 추적해야합니다. 이를 위해 캐시가 가득 찼을 때 디지털에서 결과를 제거하는 데 필요한 정보를 저장하는 두 번째 컬렉션이 필요하다고 생각합니다. 이 목록을 지속적으로 정렬하면 성능에 부정적인 영향을 줄 수 있습니다.

이 작업을 수행하는 더 나은 (즉, 성능이 더 좋은) 방법이 있습니까, 아니면 내가 모르는 일반적인 데이터 구조가 있습니까? 솔루션의 최적 성을 결정하기 위해 어떤 종류의 프로파일 링 / 측정을해야합니까?

답변:


12

LRU 제거 캐시 (최근에 사용한 제거)를 사용하려는 경우 사용할 데이터 구조의 조합은 다음과 같습니다.

  • 순환 연결 목록 (우선 순위 대기열로)
  • 사전

이는 이유:

  • 연결된 목록에 O (1) 삽입 및 제거 시간이 있습니다.
  • 목록이 가득 차서 추가 할당을 수행 할 필요가없는 경우 목록 노드를 재사용 할 수 있습니다.

기본 알고리즘은 다음과 같습니다.

데이터 구조

LinkedList<Node<KeyValuePair<Input,Double>>> list; Dictionary<Input,Node<KeyValuePair<Input,Double>>> dict;

  1. 입력이 접수되었습니다
  2. 사전에 키가 포함 된 경우
    • 노드에 저장된 값을 반환하고 노드를 목록의 시작 부분으로 이동
  3. 사전에 키가없는 경우
    • 가치를 계산하다
    • 목록의 마지막 노드에 값을 저장
    • 마지막 값이 없으면 사전에서 이전 키를 제거하십시오.
    • 마지막 노드를 첫 번째 위치로 이동하십시오.
    • 사전에 (입력, 노드) 키 값 쌍을 저장하십시오.

이 접근법의 일부 이점은 사전 값을 읽고 설정하는 O (1)에 접근하고, 링크 된 목록에서 노드를 삽입하고 제거하는 것이 O (1)이며, 이는 알고리즘이 값을 읽고 쓰는 데 O (1)에 접근하고 있음을 의미합니다. 메모리 할당 및 메모리 복사 작업을 방지하여 메모리 관점에서 안정적으로 만듭니다.


좋은 점, 지금까지 가장 좋은 아이디어, IMHO. 나는 오늘 이것을 기반으로 캐시를 구현했으며 내일 얼마나 잘 수행하는지 프로파일 링하고 확인해야합니다.
PersonalNexus

3

이것은 평균 PC에서 처리 할 수있는 처리 능력을 고려할 때 단일 계산을 위해 많은 노력을 기울이는 것 같습니다. 또한 각 고유 한 값 쌍에 대해 첫 번째 계산 호출 비용이 발생하므로 100,000 개의 고유 값 쌍은 여전히 최소 시간 n * 100,000의 비용이 듭니다 . 사전이 커질수록 사전의 값에 액세스하는 속도가 느려질 수 있습니다. 사전 액세스 속도가 계산 속도에 대해 합리적인 수익을 제공 할만큼 충분히 보상 할 것이라고 보장 할 수 있습니까?

어쨌든, 알고리즘을 최적화 할 수단을 찾는 것이 필요할 것 같습니다. 이를 위해서는 병목 현상의 위치를 ​​확인하고 클래스 인스턴스화, 순회 목록, 데이터베이스와 관련하여 오버 헤드를 줄일 수있는 방법이 있는지 확인하려면 Redgate Ants 와 같은 프로파일 링 도구 가 필요합니다. 액세스, 또는 그것이 무엇이든간에 많은 시간이 소요됩니다.


1
불행히도 계산 알고리즘은 당분간 CPU를 많이 사용하는 고급 수학을 사용하는 타사 라이브러리이므로 변경할 수 없습니다. 나중에 다시 작업 할 경우 제안 된 프로파일 링 도구를 확실히 확인하겠습니다. 또한 계산은 종종 동일한 입력으로 수행 될 수 있으므로 예비 프로파일 링은 매우 순진한 캐싱 전략으로도 분명한 이점을 보여줍니다.
PersonalNexus

0

한 가지 생각은 왜 n 개의 결과 만 캐시 하는가? n이 300,000 인 경우에도 7.2MB의 메모리 만 사용합니다 (테이블 구조에 필요한 추가 메모리 만 사용). 그것은 물론 세 개의 64 비트 복식을 가정합니다. 메모리 공간 부족에 대해 걱정하지 않으면 복잡한 계산 루틴 자체에 메모를 적용 할 수 있습니다.


캐시는 하나만있는 것이 아니라 내가 분석하고있는 "항목"당 하나이며, 수백만 개의 항목이있을 수 있습니다.
PersonalNexus

어떤 '항목'의 입력이 중요합니까? 부작용이 있습니까?
jk.

@jk. 다른 항목은 계산에 매우 다른 입력을 생성합니다. 이것은 중복이 거의 없다는 것을 의미하기 때문에 단일 캐시에 유지하는 것이 의미가 있다고 생각하지 않습니다. 또한 다른 항목이 다른 스레드에 존재할 수 있으므로 공유 상태를 피하기 위해 캐시를 별도로 유지하고 싶습니다.
PersonalNexus

@PersonalNexus 계산에 2 개 이상의 매개 변수가 있음을 암시하기 위해 이것을 사용합니까? 그렇지 않으면 여전히 기본적으로 f (x, y) = 몇 가지 작업이 있습니다. 또한 공유 상태는 방해가 아닌 성능에 도움이 될 것 같습니다.
피터 스미스

@PeterSmith 두 매개 변수는 기본 입력입니다. 다른 것들도 있지만 거의 변하지 않습니다. 그렇다면 전체 캐시를 버릴 것입니다. "공유 상태"란 모든 항목 또는 그룹에 대한 공유 캐시를 의미했습니다. 다른 방법으로 잠 그거나 동기화해야하므로 성능이 저하됩니다. 공유 상태의 성능 영향에 대한 추가 정보 .
PersonalNexus

0

두 번째 컬렉션의 접근 방식은 좋습니다. 최소값을 빠르게 찾거나 삭제하고 대기열 내의 우선 순위를 변경 (증가) 할 수있는 우선 순위 대기열 이어야합니다 (후자는 가장 간단한 prio 대기열 구현에서는 지원되지 않음). C5 라이브러리 가 호출 될 때, 이러한 수집이있다 IntervalHeap.

또는 물론 자신 만의 컬렉션을 만들 수도 있습니다 SortedDictionary<int, List<InputCount>>. ( 데이터와 가치를 InputCount결합한 수업이어야 함 )InputCount

카운트 값을 변경할 때 해당 컬렉션을 업데이트 하면 요소를 제거했다가 다시 삽입하여 구현할 수 있습니다.


0

Peter Smith의 답변에서 지적했듯이 구현하려는 패턴을 memoization 이라고 합니다. C #에서는 부작용없이 투명한 방식으로 메모를 구현하는 것이 매우 어렵습니다. C #의 함수형 프로그래밍에 관한 Oliver Sturm의 책은 해결책을 제시합니다 (코드는 10 장에서 다운로드 할 수 있습니다).

F #에서는 훨씬 쉬울 것입니다. 물론 다른 프로그래밍 언어를 사용하는 것은 큰 결정이지만 고려해 볼 가치가 있습니다. 특히 복잡한 계산에서는 메모보다 프로그래밍을 더 쉽게 수행해야합니다.

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