-
Notifications
You must be signed in to change notification settings - Fork 265
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
Performance of framing #8503
Comments
@remi-delmas-3000 @tautschnig Do you have ideas here? |
Hi, I don't think its a problem with framing per se (as in checking that side effects are included in the assigns clauses, checking assigns clause inclusion, etc.). What seems to be happening is that solvers struggle with quantified proof obligations. I encoded the contract transformation manually (to get rid of frame condition checking) which gives the following straight-line code: #include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#define A 12
#define B 42
#define SZ 256
void harness(void)
{
int len, start, sz;
__CPROVER_assume(2 <= len && len <= SZ && (len & 1) == 0);
__CPROVER_assume(0 <= start && start < len);
__CPROVER_assume(1 <= sz && sz <= len / 2 && start + 2 * sz <= len);
int8_t *x = malloc(len * sizeof(int8_t));
__CPROVER_assume(x != NULL);
__CPROVER_assume(__CPROVER_forall {
int i;
0 <= i && i < start ==> x[i] <= B
});
__CPROVER_assume(__CPROVER_forall {
int i;
start <= i && i < len ==> x[i] <= A
});
/// inline call to bump_A_to_B(x + start, sz);
int8_t *call_arg1 = x + start;
int call_arg2 = sz;
{
int8_t *x = call_arg1;
int sz = call_arg2;
__CPROVER_assert(
__CPROVER_forall {
int i;
0 <= i && i < 2 * sz ==> x[i] <= A
},
"precondition replaced call");
__CPROVER_havoc_slice(x, 2 * sz);
__CPROVER_assume(__CPROVER_forall {
int i;
0 <= i && i < 2 * sz ==> x[i] <= B
});
}
#ifdef POST1
__CPROVER_assert(
__CPROVER_forall {
int i;
0 <= i && i< start + 2 * sz ==> x[i] <= B
},
"post condition 1");
#endif
#ifdef POST2
__CPROVER_assert(
__CPROVER_forall {
int i;
start + 2 * sz <= i && i<len ==> x[i] <= A
},
"post condition 2");
#endif The I analyzed POST1 and POST2 separately and together, got these results: | | z3 | bw |
| ------------ | ------------ | ---------- | ---------- |
| no contracts | post 1 only | 42m41.954s | 33m2.139s |
| | post 2 only | 3m1.328s | 0m14.932s |
| | post 1+2 | 25m34.864s | 17m30.813s |
| ------------ | ------------ | ---------- | ---------- |
| contracts | post 1 only | 19m14.907s | 19m40.051s |
| | post 2 only | 0m22.193s | 2m30.510s |
| | post 1+2 | 24m3.121s | 26m26.308s | So using contracts vs the straigh-line code that does not do frame condition checking does not matter that much on the overall runtime. What we see is that solvers struggle to prove the first post condition that says that
If we split all the pre and post conditions over ranges | | z3 | bw |
| ------------ | ------------ | ---------- | ---------- |
| no contracts | post 1 only | 0m16.543s | 2m29.401s |
| | post 2 only | 10m31.124s| 32m26.411s |
| | post 3 only | 0m15.207s | 2m11.520s |
| | all | 15m24.019s| 34m1.366s |
| ------------ | ------------ | ---------- | ---------- |
| contracts | post 1 only | 0m40.772s | 2m11.435s |
| | post 2 only | 20m37.789s| 14m32.638s |
| | post 3 only | 0m26.735s | 2m27.044s |
| | all | 26m12.638s| 19m34.666s |
| ------------ | ------------ | ----------| ---------- | We see that POST1 on Encoding of arraysCBMC encodes bounded arrays in SMT as a pair of unbounded array and a length. In this program we have two versions of array (Slightly simplified from the actual encoding)
The havoc_slice operation uses an intermediate nondeterministic array and a forall constraint to define x1
The post condition on the range [start, start+2*sz) is not expressed using a quantifier, instead, we introduce a free
So really both z3 and bitwuzla struggle to establish that I've seen that the proof times are sensitive to the max array Maybe the encoding of the post condition could also use a quantifier instead of a ground constant for Any suggestions on how to improve the encoding ? |
Hi, I played a little bit more with the encoding and was able to bring the runtime down to 1 second for bitwuzla, 20 seconds for z3 with some tweaks in the encoding, while scaling to very large allocation sizes. I was able to come up with this. The main changes are:
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#define A 12
#define B 42
#define SZ (__CPROVER_max_malloc_size-1)
void harness() {
size_t len, start, sz;
__CPROVER_assume(2 <= len && len <= SZ);
__CPROVER_assume((len & 1) == 0);
__CPROVER_assume(0 <= start && start < len);
__CPROVER_assume(1 <= sz && sz <= len / 2);
__CPROVER_assume(start + 2 * sz <= len);
int8_t *x = malloc(len * sizeof(int8_t));
__CPROVER_assume(x != NULL);
__CPROVER_assume(__CPROVER_forall {
size_t i;
0 <= i && i < start ==> x[i] <= B
});
__CPROVER_assume(__CPROVER_forall {
size_t i;
start <= i && i < len ==> x[i] <= A
});
// inlined call arguments
int8_t *call_arg1 = x + start;
size_t call_arg2 = sz;
{
int8_t *x = call_arg1;
size_t sz = call_arg2;
__CPROVER_assert(
__CPROVER_forall {
size_t i;
0 <= i && i < 2 * sz ==> x[i] <= A
},
"precondition replaced call");
int8_t nondet_bytes[2*sz];
// assume the post condition directly on the havoced bytes
__CPROVER_assume(__CPROVER_forall {
size_t i;
(0 <= i && i < (2 * sz)) ==> nondet_bytes[i] <= B
});
__CPROVER_array_replace(x, nondet_bytes);
}
// assert post condition
bool post1 = __CPROVER_forall {
size_t i;
(0 <= i && i < (start + 2 * sz)) ==> x[i] <= B
};
assert(post1);
bool post2 = __CPROVER_forall {
size_t i;
((start + 2 * sz) <= i && i < len) ==> x[i] <= A
};
assert(post2);
} ** 0 of 36 failed (1 iterations)
VERIFICATION SUCCESSFUL
cbmc --function harness -DENC15 hanno_simpler.c --z3 -DMAX_SIZE 18.77s user 0.18s system 98% cpu 19.153 total I think makes it easier for z3 to trigger quantifiers since guards match syntactically. Below I explain what I think are the problems with the original formulation. Problem 1 : encoding of havoc_slice + assumptionsThe contract replacement of // inlined call to bump_A_B
int8_t *call_arg1 = x + start;
size_t call_arg2 = sz;
{
int8_t *x = call_arg1;
size_t sz = call_arg2;
assert( __CPROVER_forall {
size_t i;
0 <= i && i < 2 * sz ==> x[i] <= A
});
__CPOVER_havoc_slice(x, 2 * sz);
__CPROVER_assume(__CPROVER_forall {
size_t i;
(0 <= i && i < (2 * sz)) ==> x[i] <= B
});
}
uint8_t nondet_bytes[2*sz];
__CPROVER_array_replace(x, nondet_bytes); As described above, at the SMT-level, all arrays ;; x before havoc_slice
(declare-const x!0 (Array Int Int))
(declare-const x_len!0 Int)
;; nondet byte array used by havoc_slice
(declare-const nondet_bytes (Array Int Int))
(declare-const nondet_bytes_len Int)
;; x after havoc_slice
(declare-const x!1 (Array Int Int))
(declare-const x_len!1 Int)
;; connect x!0, nondet_bytes, x!1 havoc_slice
(assert
(forall ((i Int))
(=> (and (<= 0 i) (< i x_len!1))
(= (select x!1 i)
(ite (and (<= start i) (< i (+ start (* 2 sz))))
(select nondet_bytes (- i start))
(select x!0 i)) The post condition of ;; assumption post call
(assert
(forall ((i Int))
(=> (and (<= 0 i) (< i (* 2 sz)))
(<= (select x!1 (+ start i)) B) The final proof obligation is expressed on So we assumed something like: ;; bump_A_B post condition
__CPROVER_assume(__CPROVER_forall {
size_t i;
(0 <= i && i < (2 * sz)) ==> (x+start)[i] <= B
});
} and we are trying to prove this ;; top level post condition
__CPROVER_assert(__CPROVER_forall {
size_t i;
(start <= i && i < start + (2 * sz)) ==> x[i] <= B
});
} The Problem 2 : grounding foralls too early, losing guard structureMoreover, instead of using the exact quantified formula we specified in the source, CBMC eliminates the quantifier by introducing a witness variable for the quantified index and grounding, while also losing the implicative structure. (declare-const i Int)
(define-fun POST1 () Bool
(=> some_path_condition
(or (> start i) (>= i (+ start (* 2 sz)) (<= (select x!1 i) B)))
(assert (not POST1))
(check-sat) Given that quantifier instantiation is really dependent on syntactic triggers I suspect that the z3 or bitwuzla cannot trigger anything anymore and are stuck enumerating counter examples and learning clauses one by one indirectly rather that discharging a forall by grouding a single forall. Problem 3: indexing into arrays with
|
I'm observing that wrapping a function operating on a buffer by another function which merely 'frames' it to operate on a slice of the buffer, leads to very slow proof times.
Here is a simple example, where the sub-function bumps a bound on the sub buffer. This is a minimal version of an example arising in practice when verifying the Number Theoretic Transform (pq-code-package/mlkem-native#371).
I was expecting the proof to be almost instantaneous, but for
SZ==256
, which is the value I am working with in practice, I have not yet seen it terminate.The issue is the same between Z3 and Bitwuzla.
CBMC Version 6.3.1
The text was updated successfully, but these errors were encountered: