Создание уникальных, сопоставимых значений

Каков хороший способ генерировать специальные ключи, где каждый ключ уникален для программы? В идеале действие вида:

newKey :: IO Key

так что:

do a <- newKey
   b <- newKey
   return (a == b)

всегда возвращает ложь. Кроме того, должна быть возможность использовать Key в эффективном ассоциативном контейнере (например, Map).

Это можно использовать, например, для поддержки набора обработчиков событий, поддерживающих случайную вставку и удаление:

Map Key EventHandler

Варианты, о которых я знаю:

  • newIORef(): удовлетворяет указанному выше инварианту, но IORef не имеет Ord экземпляр.

  • malloc: быстро и Ptr () имеет экземпляр Ord, но результат не является сборщиком мусора.

  • newStablePtr(): не выполняется сборка мусора и StablePtr не имеет экземпляра Ord.

  • newIORef() >>=makeStableName: Должен удовлетворять указанному выше инварианту и подлежит сборке мусора, но более сложный в использовании (требует от меня использования хеш-таблицы).

  • mallocForeignPtrBytes1: Удовлетворяет обоим критериям. , но эффективно ли это?

mallocForeignPtrBytes 1 кажется моим лучшим вариантом. Я полагаю, что мог бы сделать его немного более эффективным, используя непосредственно GHC newPinnedByteArray# primop.

Есть ли лучшие варианты? Является ли подход mallocForeignPtrBytes ошибочным по какой-то неочевидной причине?


person Joey Adams    schedule 24.12.2011    source источник
comment
Другой вариант: пакет uuid на hackage.   -  person Dan Burton    schedule 24.12.2011


Ответы (4)


Если вы не хотите добавлять какие-либо дополнительные зависимости в свой проект, вы можете использовать Data.Unique в базовом пакете.

Внутри система использует TVar (который опирается на систему STM GHC) из Integers, так что каждый раз, когда вы вызываете newUnique, TVar увеличивается атомарно, а новый Integer сохраняется в непрозрачном типе данных Unique. Поскольку TVar не могут быть изменены разными потоками одновременно, они гарантируют, что Unique генерируются последовательно и фактически должны быть уникальными.

person dflemstr    schedule 24.12.2011
comment
Я немного удивлен, что он не построен на atomicModifyIORef, который я всегда считал самым быстрым способом выполнения относительно простых атомарных операций. - person Louis Wasserman; 24.12.2011
comment
@LouisWasserman: Посмотрите на источник. Комментарии объясняют, почему они не пошли по этому пути: использование atomicModifyIORef было бы немного быстрее, но может страдать от неблагоприятных проблем с планированием (см. #3838) - person Joey Adams; 24.12.2011
comment
Это означает, что значения уникальны при выполнении одной программы, но несколько экземпляров программы могут генерировать одни и те же уникальные значения, верно? - person Dan Burton; 24.12.2011
comment
Я принял этот ответ, потому что Data.Unique довольно быстрый и уже доступен в базе. Хотя newPinnedByteArray быстрее, на практике это вряд ли что-то изменит (или даже может привести к резкому изменению производительности от запуска к запуску). Посмотрите мои тесты. - person Joey Adams; 24.12.2011

На hackage есть несколько соответствующих пакетов. пакет concurrent-supply выглядит довольно тщательно разработанным.

person Anthony    schedule 24.12.2011

Спасибо, dflemstr, за указание Data.Unique. Я сравнил Data.Unique с парой альтернативных реализаций:

Уникальный2.hs

На основе mallocForeignPtrBytes< /а>:

{-# LANGUAGE DeriveDataTypeable #-}
module Unique2 (Unique2, newUnique2) where

import Control.Applicative
import Data.Typeable (Typeable)
import Data.Word (Word8)
import Foreign.ForeignPtr

newtype Unique2 = Unique2 (ForeignPtr Word8)
    deriving (Eq, Ord, Typeable)

newUnique2 :: IO Unique2
newUnique2 = Unique2 <$> mallocForeignPtrBytes 1

Уникальный3.hs

На основе newPinnedByteArray, который используется внутри mallocForeignPtrBytes. Это в основном то же самое, что и Unique2, за исключением некоторых накладных расходов.

{-# LANGUAGE DeriveDataTypeable #-}
module Unique3 (Unique3, newUnique3) where

import Control.Applicative
import Data.Primitive.ByteArray
import Data.Primitive.Types
import Data.Typeable

newtype Unique3 = Unique3 Addr
    deriving (Eq, Ord, Typeable)

newUnique3 :: IO Unique3
newUnique3 = Unique3 . mutableByteArrayContents <$> newPinnedByteArray 1

уникальный-benchmark.hs

import Criterion.Main

import Control.Exception (evaluate)
import Control.Monad
import Data.Unique
import Unique2
import Unique3

import qualified Data.Set as S

main :: IO ()
main = defaultMain
    [ bench "Unique" $
        replicateM 10000 newUnique  >>= evaluate . S.size . S.fromList
    , bench "Unique2" $
        replicateM 10000 newUnique2 >>= evaluate . S.size . S.fromList
    , bench "Unique3" $
        replicateM 10000 newUnique3 >>= evaluate . S.size . S.fromList
    ]

Полученные результаты:

Скомпилировано с ghc -Wall -O2 -threaded -fforce-recomp unique-benchmark.hs:

benchmarking Unique
mean: 15.75516 ms, lb 15.62392 ms, ub 15.87852 ms, ci 0.950
std dev: 651.5598 us, lb 568.6396 us, ub 761.7921 us, ci 0.950

benchmarking Unique2
mean: 20.41976 ms, lb 20.11922 ms, ub 20.67800 ms, ci 0.950
std dev: 1.427356 ms, lb 1.254366 ms, ub 1.607923 ms, ci 0.950

benchmarking Unique3
mean: 14.30127 ms, lb 14.17271 ms, ub 14.42338 ms, ci 0.950
std dev: 643.1826 us, lb 568.2373 us, ub 737.8637 us, ci 0.950

Если я увеличу величину с 10000 до 100000:

benchmarking Unique
collecting 100 samples, 1 iterations each, in estimated 21.26808 s
mean: 206.9683 ms, lb 206.5749 ms, ub 207.7638 ms, ci 0.950
std dev: 2.738490 ms, lb 1.602821 ms, ub 4.941017 ms, ci 0.950

benchmarking Unique2
collecting 100 samples, 1 iterations each, in estimated 33.76100 s
mean: 319.7642 ms, lb 316.2466 ms, ub 323.2613 ms, ci 0.950
std dev: 17.94974 ms, lb 16.93904 ms, ub 19.34948 ms, ci 0.950

benchmarking Unique3
collecting 100 samples, 1 iterations each, in estimated 21.22741 s
mean: 200.0456 ms, lb 199.2538 ms, ub 200.9107 ms, ci 0.950
std dev: 4.231600 ms, lb 3.840245 ms, ub 4.679455 ms, ci 0.950

Вывод:

Data.Unique (встроенная реализация) и Unique3 (на основе newPinnedByteArray) идут ноздря в ноздрю в этих тестах. newUnique3 сам по себе более чем в десять раз быстрее, чем newUnique, но накладные расходы на генерацию ключей ничтожны по сравнению с затратами на использование.

Unique2 проигрывает из-за накладных расходов на оболочку, но между Data.Unique и Unique3 мои результаты неубедительны. Я бы рекомендовал Data.Unique просто потому, что он уже доступен в базе. Однако, если вы изо всех сил пытаетесь выжать последнюю часть производительности из какого-либо приложения, попробуйте заменить Data.Unique на Unique3 и расскажите мне, как это работает.

person Community    schedule 24.12.2011
comment
Я думаю, что Unique3 небезопасно, так как память будет собрана сборщиком мусора и повторно использована, так как вы не держитесь за массив. - person ehird; 25.12.2011

Один из способов, который я видел, если вы хотите, чтобы это было на верхнем уровне, - это такой хак:

import Data.IORef
import System.IO.Unsafe

newtype Key = Key Integer deriving (Ord, Eq, Show)

newKey :: IO Key
{-# NOINLINE newKey #-}
newKey = unsafePerformIO mkNewKey

mkNewKey :: IO (IO Key)
mkNewKey = do
  r <- newIORef 0
  return $ do
    modifyIORef r (+1)
    Key `fmap` (readIORef r)

main = do
  a <- newKey
  b <- newKey
  print (a,b)
person solrize    schedule 24.12.2011
comment
Это не работает, когда у вас есть несколько потоков, которым нужны уникальные значения — поскольку modifyIORef может выполняться параллельно, вы можете получить два идентичных Key. Использование atomicModifyIORef устраняет эту проблему, но может привести к неэффективности. - person dflemstr; 24.12.2011
comment
@dflemstr, какая неэффективность? atomicModifyIORef не дорого. - person luqui; 24.12.2011
comment
@luqui Я думал, что где-то читал, что atomicModifyIORef может привести к условиям гонки при чрезмерном использовании (например, один поток, выполняющий атомарные операции в цикле, закроет все другие потоки, пытающиеся получить доступ к конкретному IORef), но я не могу найти конкретный источник этого утверждения. - person dflemstr; 24.12.2011
comment
@dfelmstr: см. hackage.haskell.org/trac/ghc/ticket/3838. Похоже, эта проблема исправлена; Data.Unique теперь использует atomicModifyIORef . - person Joey Adams; 26.03.2013