Как реализовать хеш-таблицы на функциональном языке? - PullRequest
21 голосов
/ 22 июля 2011

Есть ли способ эффективно реализовать хеш-таблицы на чисто функциональном языке? Кажется, что любое изменение в хеш-таблице потребует создания копии исходной хеш-таблицы. Я должен что-то упустить. Хеш-таблицы - чертовски важные структуры данных, и без них язык программирования был бы ограничен.

Ответы [ 3 ]

18 голосов
/ 05 июня 2012

Есть ли способ эффективно реализовать хеш-таблицы на чисто функциональном языке?

Хеш-таблицы представляют собой конкретную реализацию абстрактной структуры данных "словарь" или "ассоциативный массив".Поэтому я думаю, что вы действительно хотите спросить об эффективности чисто функциональных словарей по сравнению с императивными хеш-таблицами.

Кажется, что любое изменение в хеш-таблице потребует создания копииисходной хеш-таблицы.

Да, хеш-таблицы по своей сути являются императивными и прямого прямого функционального эквивалента не существует.Возможно, наиболее похожим чисто функциональным типом словаря является хэш trie , но он значительно медленнее хеш-таблиц из-за выделения и косвенных указаний.

Я должен что-то упустить.Хеш-таблицы - это чертовски важные структуры данных, и без них язык программирования был бы ограничен.

Словари - это очень важная структура данных (хотя стоит отметить, что они были редкостью в основном, пока Perl не сделалони популярны в 1990-х годах, поэтому люди десятилетиями кодировали вещи без использования словарей).Я согласен, что хеш-таблицы также важны, потому что они часто являются самыми эффективными словарями.

Существует много чисто функциональных словарей:

  • Сбалансированные деревья (красно-черные), AVL, сбалансированный вес, деревья пальцев и т. Д.), Например, Map в OCaml и F # и Data.Map в Haskell.

  • Хэш пытается , напримерPersistentHashMap в Clojure.

Но все эти чисто функциональные словари на намного медленнее, чем приличная хеш-таблица (например, .NET Dictionary).

Остерегайтесь бенчмарков на Haskell, сравнивающих хеш-таблицы с чисто функциональными словарями, утверждая, что чисто функциональные словари конкурентоспособны.Правильный вывод состоит в том, что хеш-таблицы на Haskell настолько неэффективны, что они почти такие же медленные, как и чисто функциональные словари.Если вы сравните, например, с .NET, вы обнаружите, что .NET Dictionary может быть в 26 раз быстрее, чем хеш-таблица Haskell !

Я думаю, чтобы действительно заключить, чтовы пытаетесь прийти к выводу о производительности Haskell, вам нужно будет протестировать больше операций, использовать нелепый тип ключа (удваивается в качестве ключей, что?), не использовать -N8 без причины и сравнивать с третьим языком, которыйтакже включает в себя его параметрические типы, такие как Java (поскольку Java имеет приемлемую производительность в большинстве случаев), чтобы увидеть, является ли это общей проблемой упаковки или более серьезной ошибкой времени выполнения GHC.Эти бенчмарки соответствуют этим принципам (и примерно в 2 раза быстрее, чем текущая реализация с хеш-таблицами).

Это именно та дезинформация, о которой я говорил.Не обращайте внимания на хеш-таблицы Haskell в этом контексте, просто посмотрите на производительность самых быстрых хеш-таблиц (т.е. не Haskell) и самых быстрых чисто функциональных словарей.

8 голосов
/ 22 июля 2011

Хеш-таблицы могут быть реализованы с помощью чего-то вроде ST-монады в Haskell, которая в основном оборачивает действия ввода-вывода в чисто функциональный интерфейс.Это достигается путем принудительного выполнения действий ввода-вывода, что обеспечивает прозрачность ссылок: вы не можете получить доступ к старой «версии» хеш-таблицы.

См .: hackage.haskell.орг / пакет / Hashtables

7 голосов
/ 06 июня 2012

У всех существующих ответов есть хорошие моменты, которыми стоит поделиться, и я подумал, что просто добавлю еще один фрагмент данных в уравнение: сравнение производительности нескольких различных ассоциативных структур данных.

Тест состоит из последовательновставка, затем поиск и добавление элементов массива.Этот тест не является невероятно строгим, и его не следует воспринимать как таковой, он просто указывает на то, чего ожидать.

Сначала в Java используется HashMap несинхронизированная Map реализация:

import java.util.Map;
import java.util.HashMap;

class HashTest {
    public static void main (String[] args)
    {
        Map <Integer, Integer> map = new HashMap<Integer, Integer> ();
        int n = Integer.parseInt (args [0]);
        for (int i = 0; i < n; i++)
            {
                map.put (i, i);
            }

        int sum = 0;
        for (int i = 0; i < n; i++)
            {
                sum += map.get (i);
            }


        System.out.println ("" + sum);
    }
}

Затем реализация на Haskell с использованием недавней работы с хеш-таблицами, проделанной Грегори Коллинзом (в пакете hashtables).Это может быть как чисто (через ST монаду), так и нечисто через IO, я использую версию IO здесь:

{-# LANGUAGE ScopedTypeVariables, BangPatterns #-}
module Main where

import Control.Monad
import qualified Data.HashTable.IO as HashTable
import System.Environment

main :: IO ()
main = do
  n <- read `fmap` head `fmap` getArgs
  ht :: HashTable.BasicHashTable Int Int <- HashTable.new
  mapM_ (\v -> HashTable.insert ht v v) [0 .. n - 1]
  x <- foldM (\ !s i -> HashTable.lookup ht i >>=
               maybe undefined (return . (s +)))
       (0 :: Int) [0 .. n - 1]
  print x

Наконец, одна использует неизменную реализацию HashMapот hackage (из пакета hashmap):

module Main where

import Data.List (foldl')
import qualified Data.HashMap as HashMap
import System.Environment

main :: IO ()
main = do
  n <- read `fmap` head `fmap` getArgs
  let
    hashmap = 
        foldl' (\ht v -> HashMap.insert v v ht) 
           HashMap.empty [0 :: Int .. n - 1]
  let x = foldl' (\ s i -> hashmap HashMap.! i + s) 0 [0 .. n - 1]
  print x

Исследуя производительность для n = 10 000 000, я обнаружил, что общее время работы выглядит следующим образом:

  • Java HashMap -- 24,387 с
  • Haskell HashTable - 7,705 с, 41% времени в GC (
  • Haskell HashMap - 9,368 с, 62% времени в GC

Knockingдо n = 1 000 000, мы получаем:

  • Java HashMap - 0,700 с
  • Haskell HashTable - 0,723 с
  • Haskell HashMap - 0,789 с

Это интересно по двум причинам:

  1. Производительность, как правило, довольно близка (за исключением случаев, когда Java расходится более 1М записей)
  2. Огромное количество временитратится на сбор! (убийство Java в случае n = 10 000 000).

Это может указывать на то, что в языкахкак Haskell и Java, которые упаковали ключи карты, видят большой удар от этого бокса.Языки, которые либо не нуждаются, либо могут распаковать ключи и значения, вероятно, увидят в два раза большую производительность.

Очевидно, что эти реализации не самые быстрые, но я бы сказал, что используя Java в качестве базовой линии, онинаименее приемлемый / пригодный для использования во многих целях (хотя, возможно, кто-то, более знакомый с мудростью Java, мог бы сказать, считается ли HashMap разумным).

Я бы отметил, что Haskell HashMap занимает много места по сравнению с HashTable.

Программы на Haskell были скомпилированы с GHC 7.0.3 и -O2 -threaded и работают только с флагом +RTS -s для статистики GC во время выполнения.Java была скомпилирована с OpenJDK 1.7.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...