Эффективный способ выполнять n-арные ветвления / табличные функции?
Я пытаюсь получить базовую информацию о характеристиках производительности веток в SBV.
Предположим, у меня есть SInt16
и очень разреженная таблица поиска Map Int16 a
. Я могу реализовать поиск с помощью вложенныхite
:
sCase :: (Mergeable a) => SInt16 -> a -> Map Int16 a -> a
sCase x def = go . toList
where
go [] = def
go ((k,v):kvs) = ite (x .== literal k) v (go kvs)
Однако это означает, что сгенерированное дерево будет очень глубоким.
- Это имеет значение?
- Если да, то лучше ли вместо этого создать сбалансированное дерево ветвей, эффективно отражающее
Map
структура? Или есть какая-то другая схема, которая дала бы еще лучшую производительность? - Если на карте меньше 256 записей, изменится ли она что-нибудь, чтобы "сжать" ее так, чтобы
sCase
работает надSInt8
иMap Int8 a
? - Есть ли какой-нибудь встроенный комбинатор SBV для этого варианта использования, который работает лучше, чем повторение
ite
?
РЕДАКТИРОВАТЬ: Оказывается, очень важно то, что у меняa
есть, поэтому позвольте мне добавить к этому несколько деталей. Я сейчас используюsCase
для перехода в вычислении с сохранением состояния, смоделированном как RWS r w s a
, со следующими экземплярами:
instance forall a. Mergeable a => Mergeable (Identity a) where
symbolicMerge force cond thn els = Identity $ symbolicMerge force cond (runIdentity thn) (runIdentity els)
instance (Mergeable s, Mergeable w, Mergeable a, forall a. Mergeable a => Mergeable (m a)) => Mergeable (RWST r w s m a) where
symbolicMerge force cond thn els = Lazy.RWST $
symbolicMerge force cond (runRWST thn) (runRWST els)
Так что убирая все newtype
s, я хотел бы перейти к чему-нибудь типа r -> s -> (a, s, w)
ул Mergeable s
, Mergeable w
а также Mergeable a
.
1 ответ
Символический поиск стоит дорого
Поиск в символьном массиве будет дорогостоящим независимо от того, какую структуру данных вы используете. Это сводится к тому факту, что механизму символического выполнения не доступна информация, позволяющая сократить пространство состояний, поэтому он в конечном итоге выполняет более или менее то, что вы сами кодировали.
SMTLib Массивы
Однако лучшим решением в таких случаях является использование поддержки SMT для массивов: http://smtlib.cs.uiowa.edu/theories-ArraysEx.shtml
Массивы SMTLib отличаются от того, что вы бы рассматривали как массив на обычном языке программирования: они не имеют границ. В этом смысле это скорее карта от входов к выходам, охватывающая весь домен. (т. е. они эквивалентны функциям.) Но у SMT есть собственные теории для работы с массивами, и поэтому они могут справляться с проблемами, связанными с массивами, гораздо более эффективно. (С другой стороны, нет понятия выхода индекса за границы или какого-либо управления диапазоном элементов, к которым вы можете получить доступ. Однако вы можете закодировать их самостоятельно поверх абстракции, оставив вам решать, как вы хотите обрабатывать такие недействительные доступы.)
Если вам интересно узнать больше о том, как решатели SMT работают с массивами, классическая ссылка: http://theory.stanford.edu/~arbrad/papers/arrays.pdf
Массивы в SBV
SBV поддерживает массивы через SymArray
класс: https://hackage.haskell.org/package/sbv-8.7/docs/Data-SBV.html
В
SFunArray
Тип фактически не использует массивы SMTLib. Это было разработано для поддержки решателей, которые не понимали массивы, такие как ABC: https://hackage.haskell.org/package/sbv-8.7/docs/Data-SBV.htmlВ
SArray
type полностью поддерживает массивы SMTLib: https://hackage.haskell.org/package/sbv-8.7/docs/Data-SBV.html
Между этими типами есть некоторые различия, и приведенные выше ссылки описывают их. Однако для большинства целей вы можете использовать их как взаимозаменяемые.
Преобразование карты Haskell в массив SBV
Возвращаясь к вашему исходному вопросу, я бы хотел использовать SArray
смоделировать такой взгляд. Я бы закодировал это как:
{-# LANGUAGE ScopedTypeVariables #-}
import Data.SBV
import qualified Data.Map as M
import Data.Int
-- Fill an SBV array from a map
mapToSArray :: (SymArray array, SymVal a, SymVal b) => M.Map a (SBV b) -> array a b -> array a b
mapToSArray m a = foldl (\arr (k, v) -> writeArray arr (literal k) v) a (M.toList m)
И используйте его как:
g :: Symbolic SBool
g = do let def = 0
-- get a symbolic array, initialized with def
arr <- newArray "myArray" (Just def)
let m :: M.Map Int16 SInt16
m = M.fromList [(5, 2), (10, 5)]
-- Fill the array from the map
let arr' :: SArray Int16 Int16 = mapToSArray m arr
-- A simple problem:
idx1 <- free "idx1"
idx2 <- free "idx2"
pure $ 2 * readArray arr' idx1 + 1 .== readArray arr' idx2
Когда я запускаю это, я получаю:
*Main> sat g
Satisfiable. Model:
idx1 = 5 :: Int16
idx2 = 10 :: Int16
Вы можете запустить его как satWith z3{verbose=True} g
чтобы увидеть выходные данные SMTLib, которые он генерирует, что позволяет избежать дорогостоящих поисков, просто делегируя эти задачи бэкэнд-решателю.
Эффективность
Вопрос о том, будет ли это "эффективным", на самом деле зависит от того, из скольких элементов на вашей карте вы строите массив. Чем больше количество элементов и чем сложнее ограничения, тем менее эффективным он будет. В частности, если вы когда-нибудь напишете индекс, который является символическим, я бы ожидал замедления времени решения. Если все они константы, производительность должна быть относительно высокой. Как это обычно бывает в символическом программировании, действительно сложно предсказать производительность, не видя реальной проблемы и не экспериментируя с ней.
Массивы в контексте запроса
Функция newArray
работает в символическом контексте. Если вы находитесь в контексте запроса, вместо этого используйтеfreshArray
: https://hackage.haskell.org/package/sbv-8.7/docs/Data-SBV-Control.html