Skip to content

Commit 8701764

Browse files
committed
Optionally lift clinit calls after Java method conversion
This lifts all clinit calls in Java function bodies to the top, thereby reducing the chances that a clinit method is repeatedly maybe-called (e.g. due to `if(x) { A.staticfield.set(1); }`, which will only run A.<clinit> if the branch is taken). If code like this occurs in a loop, for example, the static initializer could be run by symex on every iteration, creating large alias sets to account for all the possible initializer calls and sometimes producing much larger formulas than are necessary. On the other hand, with this option set, if `x` is definitely false we will nontheless needlessly run the initializer, so in some circumstances the option can increase costs.
1 parent 7418e73 commit 8701764

20 files changed

+243
-4
lines changed
319 Bytes
Binary file not shown.
462 Bytes
Binary file not shown.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
public class Test {
2+
3+
public static void main(boolean unknown) {
4+
5+
int total = 0;
6+
7+
for(int i = 0; i < 10; ++i) {
8+
if(unknown)
9+
total += Other.x;
10+
}
11+
12+
}
13+
14+
}
15+
16+
class Other {
17+
18+
static int x = 1;
19+
20+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CORE
2+
Test.class
3+
--function Test.main --java-lift-clinit-calls
4+
activate-multi-line-match
5+
^EXIT=0$
6+
^SIGNAL=0$
7+
Unwinding recursion java::Other\.<clinit_wrapper> iteration 1\nUnwinding loop java::Test\.main:\(Z\)V\.0 iteration 1
8+
--
9+
Unwinding recursion java::Other\.<clinit_wrapper> iteration 1\nUnwinding loop java::Test\.main:\(Z\)V\.0 iteration 2
10+
--
11+
This checks that the clinit-wrapper function is called prior to iteration 1 of the loop, but not iteration 2.
12+
319 Bytes
Binary file not shown.
511 Bytes
Binary file not shown.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
public class Test {
2+
3+
public static void main(boolean unknown, boolean other_unknown) {
4+
5+
int total = 0;
6+
7+
for(int i = 0; i < 10; ++i) {
8+
if(unknown)
9+
total += Other.x;
10+
else if(other_unknown)
11+
total -= Other.x;
12+
}
13+
14+
}
15+
16+
}
17+
18+
class Other {
19+
20+
static int x = 1;
21+
22+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CORE symex-driven-lazy-loading-expected-failure
2+
Test.class
3+
--function Test.main --java-lift-clinit-calls --show-goto-functions
4+
activate-multi-line-match
5+
^EXIT=0$
6+
^SIGNAL=0$
7+
\s*// 0 no location\n\s*Other\.<clinit_wrapper>\(\);\s*// 1 no location\s*int total;
8+
--
9+
--
10+
This checks that the clinit-wrapper function is called once, even though there are two calls in the source
11+
With symex-driven loading there's an exception check after the clinit_wrapper function finishes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CORE
2+
Test.class
3+
--function Test.main --java-lift-clinit-calls
4+
activate-multi-line-match
5+
^EXIT=0$
6+
^SIGNAL=0$
7+
Unwinding recursion java::Other\.<clinit_wrapper> iteration 1\nUnwinding loop java::Test\.main:\(ZZ\)V\.0 iteration 1
8+
--
9+
Unwinding recursion java::Other\.<clinit_wrapper> iteration 1\nUnwinding loop java::Test\.main:\(ZZ\)V\.0 iteration 2
10+
--
11+
This checks that the clinit-wrapper function is called prior to iteration 1 of the loop, but not iteration 2.
12+
319 Bytes
Binary file not shown.
527 Bytes
Binary file not shown.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
public class Test {
2+
3+
public static void main(boolean unknown, boolean other_unknown) {
4+
5+
int total = 0;
6+
7+
for(int i = 0; i < 10; ++i) {
8+
if(unknown)
9+
total += Other.x;
10+
else if(other_unknown)
11+
total -= Third.x;
12+
}
13+
14+
}
15+
16+
}
17+
18+
class Other {
19+
20+
static int x = 1;
21+
22+
}
23+
24+
class Third {
25+
26+
static int x = 1;
27+
28+
}
319 Bytes
Binary file not shown.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CORE symex-driven-lazy-loading-expected-failure
2+
Test.class
3+
--function Test.main --java-lift-clinit-calls --show-goto-functions
4+
activate-multi-line-match
5+
^EXIT=0$
6+
^SIGNAL=0$
7+
\s*// 0 no location\n\s*Other\.<clinit_wrapper>\(\);\s*// 1 no location\s*Third\.<clinit_wrapper>\(\);\s*// 2 no location\s*int total;
8+
--
9+
--
10+
This checks that the clinit-wrapper function is called once, even though there are two calls in the source
11+
With symex-driven loading there's an exception check after the clinit_wrapper function
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CORE
2+
Test.class
3+
--function Test.main --java-lift-clinit-calls
4+
activate-multi-line-match
5+
^EXIT=0$
6+
^SIGNAL=0$
7+
Unwinding recursion java::Other\.<clinit_wrapper> iteration 1\nUnwinding recursion java::Third\.<clinit_wrapper> iteration 1\nUnwinding loop java::Test\.main:\(ZZ\)V\.0 iteration 1
8+
--
9+
Unwinding recursion java::Other\.<clinit_wrapper> iteration 1\nUnwinding recursion java::Third\.<clinit_wrapper> iteration 1\nUnwinding loop java::Test\.main:\(ZZ\)V\.0 iteration 2
10+
--
11+
This checks that the clinit-wrapper functions are called prior to iteration 1 of the loop, but not iteration 2.
12+

jbmc/src/java_bytecode/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ SRC = assignments_from_json.cpp \
4343
java_trace_validation.cpp \
4444
java_types.cpp \
4545
java_utils.cpp \
46+
lift_clinit_calls.cpp \
4647
lambda_synthesis.cpp \
4748
load_method_by_regex.cpp \
4849
mz_zip_archive.cpp \

jbmc/src/java_bytecode/java_bytecode_language.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Author: Daniel Kroening, [email protected]
4242
#include "java_string_literals.h"
4343
#include "java_utils.h"
4444
#include "lambda_synthesis.h"
45+
#include "lift_clinit_calls.h"
4546

4647
#include "expr2java.h"
4748
#include "load_method_by_regex.h"
@@ -104,6 +105,8 @@ void parse_java_language_options(const cmdlinet &cmd, optionst &options)
104105
{
105106
options.set_option("static-values", cmd.get_value("static-values"));
106107
}
108+
options.set_option(
109+
"java-lift-clinit-calls", cmd.isset("java-lift-clinit-calls"));
107110
}
108111

109112
prefix_filtert get_context(const optionst &options)
@@ -247,6 +250,8 @@ java_bytecode_language_optionst::java_bytecode_language_optionst(
247250

248251
if(options.is_set("context-include") || options.is_set("context-exclude"))
249252
method_context = get_context(options);
253+
254+
should_lift_clinit_calls = options.get_bool_option("java-lift-clinit-calls");
250255
}
251256

252257
/// Consume options that are java bytecode specific.
@@ -1174,7 +1179,8 @@ static void notify_static_method_calls(
11741179
}
11751180

11761181
/// \brief Convert a method (one whose type is known but whose body hasn't
1177-
/// been converted) but don't run typecheck, etc
1182+
/// been converted) if it doesn't already have a body, and lift clinit calls
1183+
/// if requested by the user.
11781184
/// \remarks Amends the symbol table entry for function `function_id`, which
11791185
/// should be a method provided by this instance of `java_bytecode_languaget`
11801186
/// to have a value representing the method body.
@@ -1200,6 +1206,13 @@ bool java_bytecode_languaget::convert_single_method(
12001206
bool ret = convert_single_method_code(
12011207
function_id, symbol_table, needed_lazy_methods, class_to_declared_symbols);
12021208

1209+
if(symbol.value.is_not_nil() && language_options->should_lift_clinit_calls)
1210+
{
1211+
auto &writable_symbol = symbol_table.get_writeable_ref(function_id);
1212+
writable_symbol.value =
1213+
lift_clinit_calls(std::move(to_code(writable_symbol.value)));
1214+
}
1215+
12031216
INVARIANT(declaring_class(symbol), "Method must have a declaring class.");
12041217

12051218
return ret;

jbmc/src/java_bytecode/java_bytecode_language.h

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ Author: Daniel Kroening, [email protected]
4949
"(lazy-methods-extra-entry-point):" \
5050
"(java-load-class):" \
5151
"(java-no-load-class):" \
52-
"(static-values):"
52+
"(static-values):" \
53+
"(java-lift-clinit-calls)"
5354

5455
#define JAVA_BYTECODE_LANGUAGE_OPTIONS_HELP /*NOLINT*/ \
5556
" --disable-uncaught-exception-check\n" \
@@ -117,7 +118,12 @@ Author: Daniel Kroening, [email protected]
117118
" instead of calling the normal static initializer\n"/* NOLINT(*) */ \
118119
" (clinit) method.\n" \
119120
" The argument can be a relative or absolute path to\n"/* NOLINT(*) */ \
120-
" the file.\n"
121+
" the file.\n" \
122+
" --java-lift-clinit-calls Lifts clinit calls in function bodies to the top of the\n" /* NOLINT(*) */ \
123+
" function. This may reduce the overall cost of static\n" /* NOLINT(*) */ \
124+
" initialisation, but may be unsound if there are\n" /* NOLINT(*) */ \
125+
" cyclic dependencies between static initializers due\n" /* NOLINT(*) */ \
126+
" to potentially changing their order of execution." /* NOLINT(*) */
121127
// clang-format on
122128

123129
class symbolt;
@@ -200,6 +206,11 @@ struct java_bytecode_language_optionst
200206
/// same kind of "return nondet null or instance of return type" body that we
201207
/// use for stubbed methods. The original method body will never be loaded.
202208
optionalt<prefix_filtert> method_context;
209+
210+
/// Should we lift clinit calls in function bodies to the top? For example,
211+
/// turning `if(x) A.clinit() else B.clinit()` into
212+
/// `A.clinit(); B.clinit(); if(x) ...`
213+
bool should_lift_clinit_calls;
203214
};
204215

205216
#define JAVA_CLASS_MODEL_SUFFIX "@class_model"
@@ -307,7 +318,6 @@ class java_bytecode_languaget:public languaget
307318
method_bytecodet method_bytecode;
308319
java_string_library_preprocesst string_preprocess;
309320

310-
311321
private:
312322
virtual std::vector<load_extra_methodst>
313323
build_extra_entry_points(const optionst &) const;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*******************************************************************\
2+
3+
Module: Static initializer call lifting
4+
5+
Author: Diffblue Ltd.
6+
7+
\*******************************************************************/
8+
9+
/// file Static initializer call lifting
10+
11+
#include "lift_clinit_calls.h"
12+
13+
#include <java_bytecode/java_static_initializers.h>
14+
15+
#include <util/expr_iterator.h>
16+
17+
#include <algorithm>
18+
19+
codet lift_clinit_calls(codet input)
20+
{
21+
// 1. Gather any clinit calls present in `input`:
22+
std::vector<symbol_exprt> clinit_wrappers_called;
23+
24+
for(auto it = input.depth_begin(), itend = input.depth_end(); it != itend;
25+
++it)
26+
{
27+
if(const auto code = expr_try_dynamic_cast<codet>(*it))
28+
{
29+
if(code->get_statement() == ID_function_call)
30+
{
31+
if(
32+
const auto callee = expr_try_dynamic_cast<symbol_exprt>(
33+
to_code_function_call(*code).function()))
34+
{
35+
if(is_clinit_wrapper_function(callee->get_identifier()))
36+
{
37+
clinit_wrappers_called.push_back(*callee);
38+
// Replace call with skip:
39+
it.mutate() = code_skipt();
40+
}
41+
}
42+
}
43+
}
44+
}
45+
46+
if(clinit_wrappers_called.empty())
47+
return input;
48+
49+
// 2. Unique, such that each clinit method is only called once:
50+
std::sort(clinit_wrappers_called.begin(), clinit_wrappers_called.end());
51+
auto delete_after =
52+
std::unique(clinit_wrappers_called.begin(), clinit_wrappers_called.end());
53+
clinit_wrappers_called.erase(delete_after, clinit_wrappers_called.end());
54+
55+
// 3. Lift static init calls to top of function:
56+
code_blockt result;
57+
for(const auto &callee : clinit_wrappers_called)
58+
{
59+
code_function_callt new_call(callee);
60+
// Nuke the source location, now that the call doesn't really relate to any
61+
// one particular line:
62+
new_call.function().drop_source_location();
63+
result.add(new_call);
64+
}
65+
66+
result.add(std::move(input));
67+
68+
return std::move(result);
69+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*******************************************************************\
2+
3+
Module: Static initializer call lifting
4+
5+
Author: Diffblue Ltd.
6+
7+
\*******************************************************************/
8+
9+
/// file Static initializer call lifting
10+
11+
#ifndef CPROVER_JAVA_BYTECODE_LIFT_CLINIT_CALLS_H
12+
#define CPROVER_JAVA_BYTECODE_LIFT_CLINIT_CALLS_H
13+
14+
#include <util/std_code.h>
15+
16+
codet lift_clinit_calls(codet input);
17+
18+
#endif // CPROVER_JAVA_BYTECODE_LIFT_CLINIT_CALLS_H

0 commit comments

Comments
 (0)