Skip to content

Commit f71ce27

Browse files
committed
Merge branch 'master' into bugfix/GR-12657
2 parents d20264b + 64153b5 commit f71ce27

File tree

71 files changed

+3317
-294
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+3317
-294
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@
33
This changelog summarizes major changes between GraalVM versions of the Python
44
language runtime. The main focus is on user-observable behavior of the engine.
55

6+
## Version 1.0.0 RC10
7+
8+
* Improve performance of C API upcalls
9+
* Improve performance of classmethods, staticmethods, `globals()`, and `locals()`
10+
* Improve performance of various string and bytes operations
11+
* Initial support for the `_thread` builtin module (actual multi-threading is still disabled, the API defaults to a dummy implementation)
12+
* Implement the `zipimporter` module
13+
* Support assignment to `object.__class__`
14+
* Use the new Truffle filesystem API to get/set the current working directory
15+
* Attempt our best to report side-effects in KEY_INFO
16+
* The KEYS message now responds with attributes and methods, never dict keys
17+
* Support the `input` builtin
18+
* Add DEBUG launcher options for performance debugging
19+
* Ensure context isolation for file descriptors and child PIDs
20+
* Fix passing custom locals and globals through `exec` and `eval`
21+
* Fixes to builtin `help`
22+
623
## Version 1.0.0 RC9
724

825
* Support `help` in the builtin Python shell

graalpython/benchmarks/src/harness.py

Lines changed: 156 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,109 @@
5555
ATTR_TEARDOWN = '__teardown__'
5656

5757

58+
# ----------------------------------------------------------------------------------------------------------------------
59+
#
60+
# the CUSUM method adapted for warmup detection within a given threshold (initial iterations)
61+
#
62+
# ----------------------------------------------------------------------------------------------------------------------
63+
def zeros(n):
64+
return [0 for _ in range(n)]
65+
66+
67+
def append(arr, val):
68+
if isinstance(arr, list):
69+
return arr + [val]
70+
else:
71+
return [val] + arr
72+
73+
74+
def cusum(values, threshold=1.0, drift=0.0):
75+
csum_pos, csum_neg = zeros(len(values)), zeros(len(values))
76+
change_points = []
77+
for i in range(1, len(values)):
78+
diff = values[i] - values[i - 1]
79+
csum_pos[i] = csum_pos[i-1] + diff - drift
80+
csum_neg[i] = csum_neg[i-1] - diff - drift
81+
82+
if csum_pos[i] < 0:
83+
csum_pos[i] = 0
84+
if csum_neg[i] < 0:
85+
csum_neg[i] = 0
86+
87+
if csum_pos[i] > threshold or csum_neg[i] > threshold:
88+
change_points = append(change_points, i)
89+
csum_pos[i], csum_neg[i] = 0, 0
90+
91+
return change_points
92+
93+
94+
def avg(values):
95+
return float(sum(values)) / len(values)
96+
97+
98+
def norm(values):
99+
_max, _min = max(values), min(values)
100+
return [float(v - _min) / (_max - _min) * 100.0 for v in values]
101+
102+
103+
def pairwise_slopes(values, cp):
104+
return [abs(float(values[i+1] - values[i]) / float(cp[i+1] - cp[i])) for i in range(len(values)-1)]
105+
106+
107+
def last_n_percent_runs(values, n=0.1):
108+
assert 0.0 < n <= 1.0
109+
end_runs_idx = len(values) - int(len(values) * n)
110+
end_runs_idx = len(values) - 1 if end_runs_idx >= len(values) else end_runs_idx
111+
return values[end_runs_idx:], list(range(end_runs_idx, len(values)))
112+
113+
114+
def first_n_percent_runs(values, n=0.1):
115+
assert 0.0 < n <= 1.0
116+
first_run_idx = int(len(values) * n)
117+
return first_run_idx -1 if first_run_idx == len(values) else first_run_idx
118+
119+
120+
def detect_warmup(values, cp_threshold=0.03, stability_slope_grade=0.01):
121+
"""
122+
detect the point of warmup point (iteration / run)
123+
124+
:param values: the durations for each run
125+
:param cp_threshold: the percent in value difference for a point to be considered a change point (percentage)
126+
:param stability_slope_grade: the slope grade (percentage). A grade of 1% corresponds to a slope of 0.5 degrees
127+
:return: the change point or -1 if not detected
128+
"""
129+
# normalize all
130+
stability_slope_grade *= 100.0
131+
cp_threshold *= 100
132+
values = norm(values)
133+
134+
try:
135+
cp = cusum(values, threshold=cp_threshold)
136+
rolling_avg = [avg(values[i:]) for i in cp]
137+
138+
def warmup(cp_index):
139+
val_idx = cp[cp_index] + 1
140+
return val_idx if val_idx < len(values) else -1
141+
142+
# find the point where the duration avg is below the cp threshold
143+
for i, d in enumerate(rolling_avg):
144+
if d <= cp_threshold:
145+
return warmup(i)
146+
147+
# could not find something below the CP threshold (noise in the data), use the stabilisation of slopes
148+
last_n_vals, last_n_idx = last_n_percent_runs(values, 0.1)
149+
slopes = pairwise_slopes(rolling_avg + last_n_vals, cp + last_n_idx)
150+
151+
for i, d in enumerate(slopes):
152+
if d <= stability_slope_grade:
153+
return warmup(i)
154+
155+
return -1
156+
except Exception as e:
157+
print("exception occurred while detecting warmup: %s" % e)
158+
return -1
159+
160+
58161
def ccompile(name, code):
59162
from importlib import invalidate_caches
60163
from distutils.core import setup, Extension
@@ -89,7 +192,14 @@ def _as_int(value):
89192

90193

91194
class BenchRunner(object):
92-
def __init__(self, bench_file, bench_args=None, iterations=1, warmup=0):
195+
def __init__(self, bench_file, bench_args=None, iterations=1, warmup=-1, warmup_runs=0):
196+
assert isinstance(iterations, int), \
197+
"BenchRunner iterations argument must be an int, got %s instead" % iterations
198+
assert isinstance(warmup, int), \
199+
"BenchRunner warmup argument must be an int, got %s instead" % warmup
200+
assert isinstance(warmup_runs, int), \
201+
"BenchRunner warmup_runs argument must be an int, got %s instead" % warmup_runs
202+
93203
if bench_args is None:
94204
bench_args = []
95205
self.bench_module = BenchRunner.get_bench_module(bench_file)
@@ -98,10 +208,8 @@ def __init__(self, bench_file, bench_args=None, iterations=1, warmup=0):
98208
_iterations = _as_int(iterations)
99209
self._run_once = _iterations <= 1
100210
self.iterations = 1 if self._run_once else _iterations
101-
102-
assert isinstance(self.iterations, int)
103-
self.warmup = _as_int(warmup)
104-
assert isinstance(self.warmup, int)
211+
self.warmup_runs = warmup_runs if warmup_runs > 0 else 0
212+
self.warmup = warmup if warmup > 0 else -1
105213

106214
@staticmethod
107215
def get_bench_module(bench_file):
@@ -139,9 +247,10 @@ def _call_attr(self, attr_name, *args):
139247

140248
def run(self):
141249
if self._run_once:
142-
print("### %s, exactly one iteration (no warmup curves)" % (self.bench_module.__name__))
250+
print("### %s, exactly one iteration (no warmup curves)" % self.bench_module.__name__)
143251
else:
144-
print("### %s, %s warmup iterations, %s bench iterations " % (self.bench_module.__name__, self.warmup, self.iterations))
252+
print("### %s, %s warmup iterations, %s bench iterations " % (self.bench_module.__name__,
253+
self.warmup_runs, self.iterations))
145254

146255
# process the args if the processor function is defined
147256
args = self._call_attr(ATTR_PROCESS_ARGS, *self.bench_args)
@@ -159,9 +268,9 @@ def run(self):
159268
bench_func = self._get_attr(ATTR_BENCHMARK)
160269
durations = []
161270
if bench_func and hasattr(bench_func, '__call__'):
162-
if self.warmup:
163-
print("### warming up for %s iterations ... " % self.warmup)
164-
for _ in range(self.warmup):
271+
if self.warmup_runs:
272+
print("### (pre)warming up for %s iterations ... " % self.warmup_runs)
273+
for _ in range(self.warmup_runs):
165274
bench_func(*args)
166275

167276
for iteration in range(self.iterations):
@@ -173,21 +282,46 @@ def run(self):
173282
if self._run_once:
174283
print("@@@ name=%s, duration=%s" % (self.bench_module.__name__, duration_str))
175284
else:
176-
print("### iteration=%s, name=%s, duration=%s" % (iteration, self.bench_module.__name__, duration_str))
285+
print("### iteration=%s, name=%s, duration=%s" % (iteration, self.bench_module.__name__,
286+
duration_str))
177287

178288
print(_HRULE)
179289
print("### teardown ... ")
180290
self._call_attr(ATTR_TEARDOWN)
181291
print("### benchmark complete")
182292
print(_HRULE)
183-
print("### BEST duration: %.3f s" % min(durations))
184-
print("### WORST duration: %.3f s" % max(durations))
185-
print("### AVG duration: %.3f" % (sum(durations) / len(durations)))
293+
294+
# summary
295+
if self._run_once:
296+
print("### SINGLE RUN duration: %.3f s" % durations[0])
297+
else:
298+
print("### BEST duration: %.3f s" % min(durations))
299+
print("### WORST duration: %.3f s" % max(durations))
300+
print("### AVG (all runs) duration: %.3f s" % (sum(durations) / len(durations)))
301+
warmup_iter = self.warmup if self.warmup > 0 else detect_warmup(durations)
302+
# if we cannot detect a warmup starting point but we performed some pre runs, we take a starting point
303+
# after the 10% of the first runs ...
304+
if warmup_iter < 0 and self.warmup_runs > 0:
305+
print("### warmup could not be detected, but %s pre-runs were executed.\n"
306+
"### we assume the benchmark is warmed up and pick an iteration "
307+
"in the first 10%% of the runs" % self.warmup_runs)
308+
warmup_iter = first_n_percent_runs(durations, 0.1)
309+
310+
if warmup_iter > 0:
311+
print("### WARMUP %s at iteration: %d" % ("specified" if self.warmup > 0 else "detected", warmup_iter))
312+
no_warmup_durations = durations[warmup_iter:]
313+
print("### AVG (no warmup) duration: %.3f s" % (sum(no_warmup_durations) / len(no_warmup_durations)))
314+
else:
315+
print("### WARMUP iteration not specified or could not be detected")
316+
317+
print(_HRULE)
318+
print("### RAW DURATIONS: %s" % str(durations))
186319
print(_HRULE)
187320

188321

189322
def run_benchmark(args):
190-
warmup = 0
323+
warmup = -1
324+
warmup_runs = 0
191325
iterations = 1
192326
bench_file = None
193327
bench_args = []
@@ -208,6 +342,12 @@ def run_benchmark(args):
208342
elif arg.startswith("--warmup"):
209343
warmup = _as_int(arg.split("=")[1])
210344

345+
elif arg == '-r':
346+
i += 1
347+
warmup_runs = _as_int(args[i])
348+
elif arg.startswith("--warmup-runs"):
349+
warmup_runs = _as_int(arg.split("=")[1])
350+
211351
elif arg == '-p':
212352
i += 1
213353
paths = args[i].split(",")
@@ -229,7 +369,7 @@ def run_benchmark(args):
229369
else:
230370
print("### no extra module search paths specified")
231371

232-
BenchRunner(bench_file, bench_args=bench_args, iterations=iterations, warmup=warmup).run()
372+
BenchRunner(bench_file, bench_args=bench_args, iterations=iterations, warmup=warmup, warmup_runs=warmup_runs).run()
233373

234374

235375
if __name__ == '__main__':

graalpython/com.oracle.graal.python.cext/src/abstract.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ PyObject * PyNumber_Invert(PyObject *o) {
136136
return do_unaryop(o, INVERT);
137137
}
138138

139+
PyObject * PyNumber_Power(PyObject *v, PyObject *w, PyObject *z) {
140+
return UPCALL_O(PY_BUILTIN, polyglot_from_string("pow", SRC_CS), native_to_java(v), native_to_java(w), native_to_java(z));
141+
}
142+
139143
UPCALL_ID(PyNumber_Index);
140144
PyObject * PyNumber_Index(PyObject *o) {
141145
if (o == NULL) {

graalpython/com.oracle.graal.python.cext/src/capi.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,13 @@ declare_type(PyFloat_Type, float, PyFloatObject);
129129
declare_type(PySlice_Type, slice, PySliceObject);
130130
declare_type(PyByteArray_Type, bytearray, PyByteArrayObject);
131131
declare_type(PyCFunction_Type, builtin_function_or_method, PyCFunctionObject);
132+
declare_type(PyWrapperDescr_Type, method_descriptor, PyWrapperDescrObject); // LS: previously wrapper_descriptor
133+
// tfel: Both method_descriptor maps to both PyWrapperDescr_Type and
134+
// PyMethodDescr_Type. This reflects our interpreter, but we need to make sure
135+
// that the dynamic type for method_descriptor is always going to be
136+
// PyMethodDescr_Type, so these two declarations cannot be in the wrong order
132137
declare_type(PyMethodDescr_Type, method_descriptor, PyMethodDescrObject);
133138
declare_type(PyGetSetDescr_Type, getset_descriptor, PyGetSetDescrObject);
134-
declare_type(PyWrapperDescr_Type, method_descriptor, PyWrapperDescrObject); // LS: previously wrapper_descriptor
135139
declare_type(PyMemberDescr_Type, property, PyMemberDescrObject); // LS: previously member_descriptor
136140
declare_type(_PyExc_BaseException, BaseException, PyBaseExceptionObject);
137141
declare_type(PyBuffer_Type, buffer, PyBufferDecorator);
@@ -298,6 +302,10 @@ const char* PyTruffle_StringToCstr(void* o, int32_t strLen) {
298302
return str;
299303
}
300304

305+
const char* PyTruffle_CstrToString(const char* o) {
306+
return polyglot_from_string(o, SRC_CS);
307+
}
308+
301309
#define PRIMITIVE_ARRAY_TO_NATIVE(__jtype__, __ctype__, __polyglot_type__, __element_cast__) \
302310
void* PyTruffle_##__jtype__##ArrayToNative(const void* jarray, int64_t len) { \
303311
int64_t i; \

graalpython/com.oracle.graal.python.cext/src/dictobject.c

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,47 @@ int PyDict_Update(PyObject *a, PyObject *b) {
122122
return 0;
123123
}
124124
}
125+
126+
PyObject* _PyObject_GenericGetDict(PyObject* obj) {
127+
PyObject** dictptr = _PyObject_GetDictPtr(obj);
128+
if (dictptr == NULL) {
129+
return NULL;
130+
}
131+
PyObject* dict = *dictptr;
132+
if (dict == NULL) {
133+
*dictptr = dict = PyDict_New();
134+
}
135+
return dict;
136+
}
137+
138+
PyObject* PyObject_GenericGetDict(PyObject* obj, void* context) {
139+
PyObject* d = _PyObject_GenericGetDict(obj);
140+
if (d == NULL) {
141+
PyErr_SetString(PyExc_AttributeError, "This object has no __dict__");
142+
}
143+
return d;
144+
}
145+
146+
PyObject** _PyObject_GetDictPtr(PyObject* obj) {
147+
Py_ssize_t dictoffset;
148+
PyTypeObject *tp = Py_TYPE(obj);
149+
150+
dictoffset = tp->tp_dictoffset;
151+
if (dictoffset == 0) {
152+
return NULL;
153+
}
154+
if (dictoffset < 0) {
155+
Py_ssize_t nitems = ((PyVarObject *)obj)->ob_size;
156+
if (nitems < 0) {
157+
nitems = -nitems;
158+
}
159+
160+
size_t size = tp->tp_basicsize + nitems * tp->tp_itemsize;
161+
if (size % SIZEOF_VOID_P != 0) {
162+
// round to full pointer boundary
163+
size += SIZEOF_VOID_P - (size % SIZEOF_VOID_P);
164+
}
165+
dictoffset += (long)size;
166+
}
167+
return (PyObject **) ((char *)obj + dictoffset);
168+
}

graalpython/com.oracle.graal.python.cext/src/typeobject.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ int PyType_Ready(PyTypeObject* cls) {
232232
PyDict_SetItemString(native_members, "tp_name", polyglot_from_string(cls->tp_name, SRC_CS));
233233
PyDict_SetItemString(native_members, "tp_doc", polyglot_from_string(cls->tp_doc ? cls->tp_doc : "", SRC_CS));
234234
PyDict_SetItemString(native_members, "tp_basicsize", PyLong_FromSsize_t(cls->tp_basicsize));
235+
PyDict_SetItemString(native_members, "tp_itemsize", PyLong_FromSsize_t(cls->tp_itemsize));
236+
PyDict_SetItemString(native_members, "tp_dictoffset", PyLong_FromSsize_t(cls->tp_dictoffset));
235237
const char* class_name = cls->tp_name;
236238
PyTypeObject* javacls = polyglot_invoke(PY_TRUFFLE_CEXT,
237239
"PyType_Ready",

0 commit comments

Comments
 (0)