3

I'm trying to get some basic information on the performance characteristics of branches in SBV.

Let's suppose I have an SInt16 and a very sparse lookup table Map Int16 a. I can implement the lookup with nested 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)

However, this means the generated tree will be very deep.

  1. Does that matter?
  2. If yes, is it better to instead generate a balanced tree of branches, effectively mirroring the Map's structure? Or is there some other scheme that would give even better performance?
  3. If there are less than 256 entries in the map, would it change anything to "compress" it so that sCase works on an SInt8 and a Map Int8 a?
  4. Is there some built-in SBV combinator for this use case that works better than iterated ite?

EDIT: It turns out that it matters a lot what my a is, so let me add some more detail to that. I am currently using sCase to branch in a stateful computation modeled as an RWS r w s a, with the following instances:

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)

So stripping away all the newtypes, I'd like to branch into something of type r -> s -> (a, s, w) s.t. Mergeable s, Mergeable w and Mergeable a.

Cactus
  • 27,075
  • 9
  • 69
  • 149

1 Answers1

1

Symbolic look-ups are expensive

Symbolic array lookup will be expensive regardless of what data-structure you use. It boils down to the fact that there's no information available to the symbolic execution engine to cut-down on the state-space, so it ends up doing more or less what you coded yourself.

SMTLib Arrays

However, the best solution in these cases is to actually use SMT's support for arrays: http://smtlib.cs.uiowa.edu/theories-ArraysEx.shtml

SMTLib arrays are different than what you'd consider as an array in a regular programming language: It does not have bounds. In that sense, it's more of a map from inputs to outputs, spanning the entire domain. (i.e., they are equivalent to functions.) But SMT has custom theories to deal with arrays and thus they can handle problems involving arrays much more efficiently. (On the down-side, there's no notion of index-out-of-bounds or somehow controlling the range of elements you can access. You can code those up yourself on top of the abstraction though, leaving it up to you to decide how you want to handle such invalid accesses.)

If you are interested in learning more about how SMT solvers deal with arrays, the classic reference is: http://theory.stanford.edu/~arbrad/papers/arrays.pdf

Arrays in SBV

SBV supports arrays, through the SymArray class: https://hackage.haskell.org/package/sbv-8.7/docs/Data-SBV.html#t:SymArray

There are some differences between these types, and the above links describe them. However, for most purposes, you can use them interchangeably.

Converting a Haskell map to an SBV array

Going back to your original question, I'd be tempted to use an SArray to model such a look up. I'd code it as:

{-# 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)

And use it as:

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

When I run this, I get:

*Main> sat g
Satisfiable. Model:
  idx1 =  5 :: Int16
  idx2 = 10 :: Int16

You can run it as satWith z3{verbose=True} g to see the SMTLib output it generates, which avoids costly lookups by simply delegating those tasks to the backend solver.

Efficiency

The question of whether this will be "efficient" really depends on how many elements your map has that you're constructing the array from. The larger the number of elements and the trickier the constraints, the less efficient it will be. In particular, if you ever write to an index that is symbolic, I'd expect slow-downs in solving time. If they're all constants, it should be relatively performant. As is usual in symbolic programming, it's really hard to predict any performance without seeing the actual problem and experimenting with it.

Arrays in the query context

The function newArray works in the symbolic context. If you're in a query context, instead use freshArray: https://hackage.haskell.org/package/sbv-8.7/docs/Data-SBV-Control.html#v:freshArray

alias
  • 28,120
  • 2
  • 23
  • 40
  • Wow, this is completely the opposite of my naive intuition: I would have thought that having a statically known structure of symbolic values is more efficient than a symbolic structure. – Cactus Jul 09 '20 at 02:54
  • It's unclear to me how to turn my value type (which is not of the form `SBV v`, it is some `a` such that `Mergeable a`) into something I can put in an `SArray`. – Cactus Jul 09 '20 at 03:04
  • 1
    It's efficient because the backend solver has a custom theory for it. Note that the problem is not in the representation, but when you do a "symbolic" look-up. (If all your lookups are done to concrete addresses, then you can just use the original Haskell map anyhow.) – alias Jul 09 '20 at 03:08
  • More concretely: how would I make an `SArray` that contains `SBV a -> SBV b` functions? – Cactus Jul 09 '20 at 03:09
  • Not sure about your second comment. Please post some code that illustrates the difficulty. Note that the type `SBV a` is phantom in `a` and all symbolic types have this structure. Otherwise, you can use multiple parallel structures for each component. – alias Jul 09 '20 at 03:10
  • You cannot make an array that has functions as arguments. SMTLib is a many sorted logic of first-order functions, so no such higher-order symbolic value can be stored in an array. – alias Jul 09 '20 at 03:11
  • I've posted more details. Would it make sense to run each branch and put the resulting `(a, s, w)` triples in the array? Although, that then leads me to the problem of needing to run this in the `Symbolic` monad, which isn't where I conveniently have access to the `r` and `s` arguments. – Cactus Jul 09 '20 at 03:18
  • You'll have to somehow bring it down to a first-order structure. There's simply no other way to use an SMT-array. Just to be clear: You *will* index this structure with a symbolic address, correct? Otherwise, you can just use a Haskell map. The whole discussion is predicated on the requirement that you do a symbolic look-up. If that's the case, then you'll have to flatten the higher-order structure and store `SBV`'s in the array. – alias Jul 09 '20 at 03:33
  • Of course, yes, I will address it with a symbolic address. – Cactus Jul 09 '20 at 03:35
  • 1
    Great.. If you want to use SMT-arrays, you'll have to get what you store in them to a regular symbolic value. If that's not possible, then you can use your original idea of looking it through a regular list via a linear walk. No other method will be any significantly better. – alias Jul 09 '20 at 03:44
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/217499/discussion-between-cactus-and-alias). – Cactus Jul 09 '20 at 03:46