10000 Question: Performance of BV RotateLeft when number of rotations is symbolic, or: how to extract bitvector slice with symbolic offset · Issue #7673 · Z3Prover/z3 · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Question: Performance of BV RotateLeft when number of rotations is symbolic, or: how to extract bitvector slice with symbolic offset #7673

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
smolkaj opened this issue Jun 4, 2025 · 11 comments
Labels
performance Issues that relate primarily to the performance of Z3, such as timeouts question string

Comments

@smolkaj
Copy link
Contributor
smolkaj commented Jun 4, 2025

Apologies if I'm posing this question in the wrong place -- please let me know if there is a better place to ask.

We're using the theory of bitvectors. We're trying to extract a slice/subvector of a vector b, where the length l of the slice is constant but the offset o may be symbolic. In Python notation:

b[o:o+l]

If o is a constant, using the Python API, we can use Extract and do z3.Extract(o + l - 1, o, b).
If o is symbolic, Extract will throw an error because it disallows symbolic indices, but we figured out the following workaround using RotateRight, which does allow symbolic rotation:

z3.Extract(l - 1, 0, z3.RotateRight(b, o))

However, empirically we're observing poor performance using this encoding when o is indeed symbolic.

I am wondering if this is expected and if there are any known approaches to address this issue, e.g. more clever encodings?
I am also curious how RotateRight works in the first place when the rotation is symbolic...does this just translate down to a # bit-wise case split or something more clever?

cc @qobilidop @jonathan-dilorenzo @kheradmandG

@LeventErkok
Copy link

Variable shifts/rotates result in inherently complex circuits. A classic idea is to use a barrel shifter. I use this in SBV and it performs better in general. (Though don't expect miracles!)

@smolkaj
Copy link
Contributor Author
smolkaj commented Jun 5, 2025

Thanks for the info and pointers! Would you happen to know if z3.RotateRight is implemented using a barrel shifter already, or if it's worth trying to implement this ourselves?

@LeventErkok
Copy link

That's really a question for @NikolajBjorner

When I worked on this some years ago, that wasn't the case. But maybe things have changed in the mean time.

@NikolajBjorner
Copy link
Contributor

The implementation is very inefficient.

First: It uses a call to urem instead of just extracting log(sz) bits for the rotation amount.
Second: the comment in the code suggests creating log(sz) calls to x <- m.mk_ite(bit[i], rotate_by(x, 2^i), x)

@NikolajBjorner
Copy link
Contributor

@smolkaj - does it improve?

@smolkaj
Copy link
Contributor Author
smolkaj commented Jun 9, 2025

@qobilidop will give this a try shortly, thanks a lot in advance!

@qobilidop
Copy link

I have done a quick experiment, but unfortunately it seems to become worse with the patch applied. We'll take a deeper look to further investigate.

@qobilidop
Copy link
qobilidop commented Jun 9, 2025

I did another more isolated experiment, and the patch actually helps improving the performance a lot! So it's likely there were some other issues in my previous experiment code.

For future reference, my benchmark results are:

  • Before the patch: ~ 2.97s
  • After the patch: ~ 0.12s

Here's my benchmark code:

import time
import z3


def sym_extract(
    src: z3.BitVecRef, offset: z3.BitVecRef, size: int
) -> z3.BitVecRef:
  assert src.size() >= offset.size()
  offset = z3.ZeroExt(src.size() - offset.size(), offset)
  src = z3.RotateLeft(src, offset)
  field = z3.Extract(
      high=src.size() - 1,
      low=src.size() - size,
      a=src,
  )
  return z3.simplify(field)


def run_experiment(
    src_size: int,
    len_fi
8000
eld_size: int,
    match_field_size: int,
    rep: int = 1,
) -> None:
  # Create a source buffer to solve for.
  src = z3.BitVec("src", src_size)

  offset = z3.BitVecVal(0, src_size.bit_length())
  constraint = z3.BoolVal(True)

  for _ in range(rep):
    # Extract a length field and skip over the length amount.
    len_field = sym_extract(src, offset, len_field_size)
    assert offset.size() >= len_field_size
    len_field = z3.ZeroExt(offset.size() - len_field_size, len_field)
    offset = offset + len_field.size() + len_field

    # Extract a match field and encode a constraint.
    match_field = sym_extract(src, offset, match_field_size)
    offset = offset + match_field.size()
    constraint = z3.And(
        constraint,
        match_field == z3.BitVecVal(0x42, match_field.size()),
    )
    constraint = z3.simplify(constraint)

  print("Constraint to solve:")
  print(constraint)
  solver = z3.Solver()
  solver.add(constraint)

  t0 = time.monotonic()
  print("solver.check() starts")
  solver.check()
  print(f"solver.check() finishes in {time.monotonic() - t0:.3f} seconds")


if __name__ == "__main__":
  run_experiment(
      src_size=1024,
      len_field_size=8,
      match_field_size=8,
      rep=2,
  )

Output (after the patch):

Constraint to solve:
And(Extract(1023,
            1016,
            RotateLeft(src,
                       Concat(0,
                              11 +
                              Concat(0,
                                     Extract(1023,
                                        1016,
                                        src))))) ==
    66,
    Extract(1023,
            1016,
            RotateLeft(src,
                       Concat(0,
                              30 +
                              Concat(0,
                                     Extract(1023,
                                        1016,
                                        src)) +
                              Concat(0,
                                     Extract(1023,
                                        1016,
                                        RotateLeft(src,
                                        Concat(0,
                                        19 +
                                        Concat(0,
                                        Extract(1023,
                                        1016,
                                        src))))))))) ==
    66)
solver.check() starts
solver.check() finishes in 0.115 seconds

@qobilidop
Copy link
qobilidop commented Jun 12, 2025

I did some further investigation and have a reproducible case now where the patch seems to make things worse. Before the patch, this case finishes ~ 20s. After the patch, the solver hangs in the solver.check() step. Here's my experiment code:

import time
import z3

SOLVER_SEXPR = """
(declare-fun packet_buffer () (_ BitVec 6144))
(assert (let ((a!1 (bvadd #b0000001000000
                  (concat (bvadd #b1000110
                                 ((_ extract 5638 5632) packet_buffer))
                          #b000000))))
(let ((a!2 (= ((_ extract 6135 6128)
                (ext_rotate_left packet_buffer
                                 (concat #b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
                                         a!1)))
              #x06)))
  (and (= ((_ extract 6047 6032) packet_buffer) #x86dd)
       (= ((_ extract 5983 5976) packet_buffer) #x11)
       (= ((_ extract 5695 5681) packet_buffer) #b0000001111
8000
10100)
       (= ((_ extract 5639 5632) packet_buffer) #x04)
       (= ((_ extract 5617 5617) packet_buffer) #b1)
       (= ((_ extract 5647 5640) packet_buffer) #x04)
       a!2))))
"""


if __name__ == "__main__":
  solver = z3.Solver()
  solver.from_string(SOLVER_SEXPR)
  print(solver)

  t0 = time.monotonic()
  print("solver.check() starts")
  solver.check()
  print(f"solver.check() finishes in {time.monotonic() - t0:.3f} seconds")

Output (before the patch):

[And(Extract(6047, 6032, packet_buffer) == 34525,
     Extract(5983, 5976, packet_buffer) == 17,
     Extract(5695, 5681, packet_buffer) == 500,
     Extract(5639, 5632, packet_buffer) == 4,
     Extract(5617, 5617, packet_buffer) == 1,
     Extract(5647, 5640, packet_buffer) == 4,
     Extract(6135,
             6128,
             RotateLeft(packet_buffer,
                        Concat(0,
                               64 +
                               Concat(70 +
                                      Extract(5638,
                                        5632,
                                        packet_buffer),
                                      0)))) ==
     6)]
solver.check() starts
solver.check() finishes in 20.053 seconds

@NikolajBjorner Any insights why this no longer works after the patch?

NikolajBjorner added a commit that referenced this issue Jun 12, 2025
perf fix
@NikolajBjorner
Copy link
Contributor

added perf improvement per common code path simplification

@qobilidop
Copy link

Thanks @NikolajBjorner for the lightning fast fix! With your latest patch, my experiment now finishes in 0.064 seconds, an improvement from 20.053 seconds!

I'll continue to do more testing. Might bother you again if other issue emerges. Or we'll let you know if everything works as expected on our side.

5C00
@github-actions github-actions bot added question string performance Issues that relate primarily to the performance of Z3, such as timeouts labels Jun 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Issues that relate primarily to the performance of Z3, such as timeouts question string
Projects
None yet
Development

No branches or pull requests

4 participants
0