Яку структуру даних я повинен використовувати для цієї стратегії кешування?


11

Я працюю над додатком .NET 4.0, який виконує досить дорогий розрахунок у двох пар, повертаючи дубль. Цей розрахунок проводиться для кожної з декількох тисяч позицій . Ці обчислення виконуються в a 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) для читання та запису значень в кеш-пам'ять і уникає розподілу пам'яті та блокування операцій копіювання пам'яті, роблячи її стабільною з точки зору пам'яті.


Хороші бали, найкраща ідея поки що, ІМХО. Я реалізував кеш на основі цього сьогодні, і мені доведеться проаналізувати та побачити, наскільки добре він працює завтра.
PersonalNexus

3

Це здається, що потрібно докласти чимало зусиль для одного розрахунку, враховуючи потужність обробки, що є у вас в середньому ПК. Крім того, ви все одно матимете витрати на перший дзвінок у свій розрахунок для кожної унікальної пари значень, тому 100 000 пар унікальних значень все одно обійдуться вам як мінімум Час n * 100 000. Врахуйте, що доступ до значень у вашому словнику, ймовірно, стане повільнішим, оскільки словник зростає. Чи можете ви гарантувати, що швидкість доступу до словника буде достатньо компенсована, щоб забезпечити розумну віддачу від швидкості вашого розрахунку?

Незважаючи на це, це здається, ніби вам, мабуть, доведеться розглянути питання щодо оптимізації свого алгоритму. Для цього вам знадобиться інструмент для профілювання, наприклад Redgate Ants, щоб побачити, де знаходяться вузькі місця, а також допомогти вам визначити, чи є способи зменшити деякі накладні витрати, які можуть бути пов’язані з екземплярами класів, переліками списків, базою даних доступ, або що б це не коштувало вам стільки часу.


1
На жаль, наразі алгоритм обчислення неможливо змінити, оскільки це стороння бібліотека, яка використовує деяку розширену математику, яка, природно, є інтенсивною процесором. Якщо пізніше, який буде перероблений, я обов'язково перегляну запропоновані інструменти для профілювання. Крім того, підрахунок буде виконуватись досить часто, іноді з однаковими вхідними даними, тому попереднє профілювання показало очевидну користь навіть при дуже наївній стратегії кешування.
PersonalNexus

0

Одна думка - чому тільки кешувати n результатів? Навіть якщо n становить 300 000, ви б використовували лише 7,2 МБ пам'яті (плюс що б не було додатково для структури таблиці). Це, звичайно, передбачає три 64-бітні парні. Ви можете просто застосувати запам'ятовування до самого складного методу обчислення, якщо ви не переживаєте, що не вистачить місця в пам'яті.


Буде не один кеш, а один на "елемент", який я аналізую, і цих елементів може бути кілька сотень тисяч.
PersonalNexus

Яким чином має значення, з якого пункту вводиться вхід? чи є побічні ефекти?
jk.

@jk. Різні елементи дають дуже різні вхідні дані до розрахунку. Оскільки це означає, що перекриття будуть незначними, я не думаю, що зберігати їх в одному кеші має сенс. Крім того, різні елементи можуть жити в різних потоках, тому, щоб уникнути спільного стану, я хотів би зберегти кеші окремими.
PersonalNexus

@PersonalNexus Я вважаю, що в розрахунку задіяно більше 2 параметрів? Інакше у вас все ще є f (x, y) = робити якісь речі. Плюс загальний стан здається, що це сприятиме ефективності, а не перешкоджає?
Пітер Сміт

@PeterSmith Два параметри є основними входами. Є й інші, але вони рідко змінюються. Якщо вони будуть, я б викинув весь кеш. Під "загальним станом" я мав на увазі спільний кеш для всіх або групи елементів. Оскільки це потрібно буде заблокувати чи синхронізувати іншим способом, це заважатиме продуктивності. Детальніше про ефективність наслідків спільного стану .
PersonalNexus

0

Підхід з другою колекцією чудовий. Вона повинна бути чергою пріоритетів, яка дозволяє швидко знаходити / видаляти мінімальні значення, а також змінювати (збільшуючи) пріоритети в черзі (остання частина є жорсткою, не підтримується більшістю простих реалізацій пріоритетної черги). Бібліотека С5 має таку колекцію, її називають IntervalHeap.

Або звичайно, ви можете спробувати створити власну колекцію, щось на кшталт а SortedDictionary<int, List<InputCount>>. ( InputCountповинен бути клас, що поєднує ваші Inputдані з вашим Countзначенням)

Оновлення цієї колекції при зміні значення рахунку може бути реалізовано шляхом видалення та повторної вставки елемента.


0

Як вказувалося у відповіді Пітера Сміта, модель, яку ви намагаєтеся реалізувати, називається запам'ятовуванням . У C # досить складно здійснити запам'ятовування прозорим способом без побічних ефектів. Книга Олівера Штурма з функціонального програмування на C # дає рішення (код доступний для завантаження, глава 10).

У F # було б набагато простіше. Звичайно, велике рішення почати використовувати іншу мову програмування, але, можливо, варто подумати. Особливо в складних обчисленнях, це обов'язково спрощує програмування більше, ніж запам'ятовування.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.