Memory layout of clauses in MiniSat

I have been trying to debug why some MiniSat-based solvers perform better at unit propagation than CryptoMiniSat. It took me exactly 3 full days to find out and I’d like to share this tidbit of information with everyone, as I think it might be of interest and I hardly believe many understand it.

The mystery of the faster unit propagation was that I tried my best to make my solver behave exactly as MiniSat to debug the issue, but even though it was behaving almost identically, it was still slower. It made no sense and I had to dig deeper. You have to understand that both solvers use their own memory managers. In fact, CryptoMiniSat had a memory manager before MiniSat. Memory managers are used so that the clauses are put close to one another so there is a chance that they are in the same memory page, or even better, close enough for them not to waste memory in the cache. This means that a contiguous memory space is reserved where the clauses are placed.

The code

The high-level logic when consolidating memory, i.e. removing unused clauses from this space is the following in CryptoMiniSat:

n = malloc(enough_space)
start = n
for offset in clauses:
  Clause* old_cl = base + offset
  copy(n, old_cl, cl->size())
  old_cl->new_offset = n-start
  offset = n-start
  n += cl.size()

for w in watches:
  w.offset = (base + w.offset)->new_offset

free(base)
base = start

In contrast, in MiniSat it’s:

n = malloc(enough_space) :
start = n

for w in watches:
  Clause* old_cl = base + w.offset
  if !old_cl->updated:
    copy(n, old_cl, old_cl->size())
    old_cl->new_offset = n-start
    old_cl->updated = true
    w.offset = n-start
    n += old_cl->size()
  else:
     w.offset = old_cl->new_offset

for offset in clauses:
  offset = (base + offset)->new_offset

free(base)
base = start

So the difference seemed like this: CryptoMiniSat was a bit simpler, not using the ‘updated’ flag, and MiniSat was a bit more complex. But if you look closely, you’ll discover something quite mind-bending. When I realized I had to stop working for about 10 minutes. The genius is right in there.

The trick

MiniSat puts clauses next to each other in memory that are next to each other in the watchlist. This, heuristically speaking, means that with a good chance, these clauses will be dereferenced one after the other — and one will pull in the other due to automatic pre-caching of contiguous memory blocks. Note that especially if blocking literals are used, watchlist elements tend to stay close to one another.

Conclusion

So that is how I wasted three days of my life. It was a fun chase, though. I wonder how many more are in there. I know of only one more, which I also found to be a lot of fun, and it’s about the position of the size of the clause in the clause data structure. But I’ll let you figure that one out.