Skip to content

Commit c1ee75d

Browse files
committed
perf(python): a dict lookup by a string key takes the string itself
get, get with a default, subscript read and store, and membership with a key the typer knows to be a string go through zb_dict_*_str, which hashes and compares the string without boxing it; a store boxes the key only when it is new. git-bug: 3c454b75f1d8516058d4f70a1d75c463106c456b11fdf6dfc69992396fb47050
1 parent 717f565 commit c1ee75d

4 files changed

Lines changed: 243 additions & 10 deletions

File tree

‎crates/zyntax_builtins/src/dicts.rs‎

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,152 @@ fn dict(list_type: TypeId) -> Vec<Decl> {
322322
));
323323
let insert = |k: Expr, v: Expr| expr(call("zb_dict_insert", vec![d.e(), k, v], unit()));
324324
out_decls.push(define("zb_dict_len", &[&d], i64(), vec![ret(count(d.e()))]));
325+
326+
// Lookups by a string that is not boxed, for a key the frontend
327+
// knows to be one: hashed and compared as a string, so a key
328+
// already present costs no box, and one stored is boxed then.
329+
let s = borrowed("text", string());
330+
let stored_text = |x: Expr| call("zb_box_get_str", vec![x], string());
331+
// The text is read only once the box is known to hold one.
332+
let when_stored_is = |stored: &Local, found: Expr| {
333+
when(
334+
eq(
335+
call("zb_any_category", vec![stored.e()], i64()),
336+
int(crate::dynamic::STR),
337+
),
338+
vec![when(
339+
call("zb_str_eq", vec![stored_text(stored.e()), s.e()], boolean()),
340+
vec![ret(found)],
341+
)],
342+
)
343+
};
344+
out_decls.push(define(
345+
"zb_dict_str_hash",
346+
&[&s],
347+
i64(),
348+
vec![
349+
h.decl(call("zb_str_hash", vec![s.e()], i64())),
350+
when(eq(h.e(), int(0)), vec![h.set(int(1))]),
351+
h.set(bitxor(h.e(), shr(h.e(), int(32)))),
352+
h.set(mul(h.e(), int(-7_046_029_254_386_353_131))),
353+
h.set(bitxor(h.e(), shr(h.e(), int(29)))),
354+
ret(h.e()),
355+
],
356+
));
357+
out_decls.push(define(
358+
"zb_dict_find_hashed_str",
359+
&[&d, &s, &h],
360+
i64(),
361+
vec![
362+
index.decl(index_of(d.e())),
363+
mask.decl(sub(len(index.e()), int(1))),
364+
slot.decl(bitand(h.e(), mask.e())),
365+
i.decl(int(0)),
366+
while_(
367+
le(i.e(), mask.e()),
368+
vec![
369+
entry.decl(slot_at(index.e(), slot.e())),
370+
when(lt(entry.e(), int(0)), vec![ret(int(-1))]),
371+
stored.decl(key_at(d.e(), entry.e())),
372+
when_stored_is(&stored, add(mul(entry.e(), int(2)), int(1))),
373+
slot.set(next_slot(slot.e(), mask.e())),
374+
i.add_assign(int(1)),
375+
],
376+
),
377+
ret(int(-1)),
378+
],
379+
));
380+
out_decls.push(define(
381+
"zb_dict_find_str",
382+
&[&d, &s],
383+
i64(),
384+
vec![
385+
when(
386+
unindexed(d.e()),
387+
vec![
388+
n.decl(len(d.e())),
389+
i.decl(int(1)),
390+
while_(
391+
lt(i.e(), n.e()),
392+
vec![
393+
stored.decl(at(d.e(), i.e())),
394+
when_stored_is(&stored, i.e()),
395+
i.add_assign(int(2)),
396+
],
397+
),
398+
ret(int(-1)),
399+
],
400+
),
401+
ret(call(
402+
"zb_dict_find_hashed_str",
403+
vec![d.e(), s.e(), call("zb_dict_str_hash", vec![s.e()], i64())],
404+
i64(),
405+
)),
406+
],
407+
));
408+
let find_str = || call("zb_dict_find_str", vec![d.e(), s.e()], i64());
409+
let boxed_key = || call("zb_str_to_dynamic", vec![s.e()], any());
410+
out_decls.push(define(
411+
"zb_dict_contains_str",
412+
&[&d, &s],
413+
boolean(),
414+
vec![ret(ge(find_str(), int(0)))],
415+
));
416+
out_decls.push(define(
417+
"zb_dict_get_str",
418+
&[&d, &s],
419+
any(),
420+
vec![
421+
i.decl(find_str()),
422+
when(lt(i.e(), int(0)), vec![fatal("KeyError", s.e())]),
423+
ret(at(d.e(), add(i.e(), int(1)))),
424+
],
425+
));
426+
out_decls.push(define(
427+
"zb_dict_get_default_str",
428+
&[&d, &s, &default],
429+
any(),
430+
vec![
431+
i.decl(find_str()),
432+
when(lt(i.e(), int(0)), vec![ret(default.e())]),
433+
ret(at(d.e(), add(i.e(), int(1)))),
434+
],
435+
));
436+
out_decls.push(define(
437+
"zb_dict_set_str",
438+
&[&d, &s, &v],
439+
unit(),
440+
vec![
441+
when(
442+
unindexed(d.e()),
443+
vec![
444+
i.decl(find_str()),
445+
if_(
446+
lt(i.e(), int(0)),
447+
vec![insert(boxed_key(), v.e())],
448+
vec![set_idx(d.e(), add(i.e(), int(1)), v.e())],
449+
),
450+
ret_void(),
451+
],
452+
),
453+
h.decl(call("zb_dict_str_hash", vec![s.e()], i64())),
454+
i.decl(call(
455+
"zb_dict_find_hashed_str",
456+
vec![d.e(), s.e(), h.e()],
457+
i64(),
458+
)),
459+
if_(
460+
lt(i.e(), int(0)),
461+
vec![expr(call(
462+
"zb_dict_insert_hashed",
463+
vec![d.e(), boxed_key(), v.e(), h.e()],
464+
unit(),
465+
))],
466+
vec![set_idx(d.e(), add(i.e(), int(1)), v.e())],
467+
),
468+
ret_void(),
469+
],
470+
));
325471
out_decls.push(define(
326472
"zb_dict_contains",
327473
&[&d, &key],
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{'alpha': 3, 'beta': 2, 'gamma': 1}
2+
3 2 None -1
3+
True False
4+
40 17 17 17 True False
5+
170
6+
1700 1700
7+
False 39
8+
KeyError 'key17'
9+
string one one True True
10+
{1: 'one', '1': 'changed', 2.5: 'float'}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# String keys looked up as strings: stored through a typed string or a
2+
# dynamic value, read back either way, in a small dict and one with a
3+
# table, with missing keys, defaults, membership and deletion.
4+
5+
def dynamic(v):
6+
return v
7+
8+
9+
counts = {}
10+
words = "alpha beta gamma alpha beta alpha".split(" ")
11+
for w in words:
12+
counts[w] = counts.get(w, 0) + 1
13+
print(counts)
14+
print(counts["alpha"], counts.get("beta"), counts.get("delta"), counts.get("delta", -1))
15+
print("gamma" in counts, "delta" in counts)
16+
17+
big = {}
18+
for i in range(40):
19+
big["key" + str(i)] = i
20+
key = dynamic("key17")
21+
print(len(big), big["key17"], big[key], big.get(key), "key39" in big, "key40" in big)
22+
big[key] = 170
23+
print(big["key17"])
24+
big["key17"] = 1700
25+
print(big[key], big.get("key17", 0))
26+
del big["key17"]
27+
print("key17" in big, len(big))
28+
try:
29+
print(big["key17"])
30+
except KeyError as e:
31+
print("KeyError", e)
32+
33+
mixed = {1: "one", "1": "string one", 2.5: "float"}
34+
print(mixed["1"], mixed[1], "1" in mixed, 1 in mixed)
35+
mixed["1"] = "changed"
36+
print(mixed)

‎crates/zyntax_python/src/lower.rs‎

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,9 +2467,14 @@ impl<'m> Lowerer<'m> {
24672467
call(&list_fn("set", e), vec![seq.node, i, v], Ty::None, span)
24682468
}
24692469
Ty::Dict => {
2470-
let k = self.expr_as(&sub.slice, Ty::Object)?;
2470+
let (k, by) = self.dict_key(&sub.slice)?;
24712471
let v = self.coerce(value, Ty::Object);
2472-
call("zb_dict_set", vec![seq.node, k, v], Ty::None, span)
2472+
call(
2473+
&format!("zb_dict_set{by}"),
2474+
vec![seq.node, k, v],
2475+
Ty::None,
2476+
span,
2477+
)
24732478
}
24742479
Ty::Object => {
24752480
let i = self.expr_as(&sub.slice, Ty::Object)?;
@@ -3068,6 +3073,17 @@ impl<'m> Lowerer<'m> {
30683073

30693074
/// A value as a `List<Any>` to iterate: a typed list boxed, a string
30703075
/// its characters, a dynamic value whatever it iterates as.
3076+
/// A dict key as the lookup takes it: a string as itself, for the
3077+
/// lookups that hash and compare a string without boxing it, and
3078+
/// anything else as a dynamic value. The suffix names the lookup.
3079+
fn dict_key(&mut self, e: &py::Expr) -> Result<(Node, &'static str)> {
3080+
if self.ty_of(e) == Ty::Str {
3081+
Ok((self.expr_as(e, Ty::Str)?, "_str"))
3082+
} else {
3083+
Ok((self.expr_as(e, Ty::Object)?, ""))
3084+
}
3085+
}
3086+
30713087
fn iterable(&mut self, v: Val, span: Span) -> Node {
30723088
match v.ty {
30733089
Ty::Str => {
@@ -4025,8 +4041,18 @@ impl<'m> Lowerer<'m> {
40254041
span,
40264042
)
40274043
} else if right.ty == Ty::Dict {
4028-
let item = self.coerce(left, Ty::Object);
4029-
call("zb_dict_contains", vec![right.node, item], Ty::Bool, span)
4044+
let by = if left.ty == Ty::Str { "_str" } else { "" };
4045+
let item = if left.ty == Ty::Str {
4046+
left.node
4047+
} else {
4048+
self.coerce(left, Ty::Object)
4049+
};
4050+
call(
4051+
&format!("zb_dict_contains{by}"),
4052+
vec![right.node, item],
4053+
Ty::Bool,
4054+
span,
4055+
)
40304056
} else {
40314057
let item = self.coerce(left, Ty::Object);
40324058
let container = self.coerce(right, Ty::Object);
@@ -4460,9 +4486,14 @@ impl<'m> Lowerer<'m> {
44604486
Ok(self.index_value(seq, index, ty, span))
44614487
}
44624488
Ty::Dict => {
4463-
let key = self.expr_as(&sub.slice, Ty::Object)?;
4489+
let (key, by) = self.dict_key(&sub.slice)?;
44644490
Ok(Val {
4465-
node: call("zb_dict_get", vec![seq.node, key], Ty::Object, span),
4491+
node: call(
4492+
&format!("zb_dict_get{by}"),
4493+
vec![seq.node, key],
4494+
Ty::Object,
4495+
span,
4496+
),
44664497
ty: Ty::Object,
44674498
})
44684499
}
@@ -4699,7 +4730,7 @@ impl<'m> Lowerer<'m> {
46994730
let d = receiver.node;
47004731
let node = match (name, args.len()) {
47014732
("get", 1) => {
4702-
let k = self.expr_as(&args[0], Ty::Object)?;
4733+
let (k, by) = self.dict_key(&args[0])?;
47034734
let none = self.coerce(
47044735
Val {
47054736
node: node(
@@ -4711,12 +4742,22 @@ impl<'m> Lowerer<'m> {
47114742
},
47124743
Ty::Object,
47134744
);
4714-
call("zb_dict_get_default", vec![d, k, none], Ty::Object, span)
4745+
call(
4746+
&format!("zb_dict_get_default{by}"),
4747+
vec![d, k, none],
4748+
Ty::Object,
4749+
span,
4750+
)
47154751
}
47164752
("get", 2) => {
4717-
let k = self.expr_as(&args[0], Ty::Object)?;
4753+
let (k, by) = self.dict_key(&args[0])?;
47184754
let default = self.expr_as(&args[1], Ty::Object)?;
4719-
call("zb_dict_get_default", vec![d, k, default], Ty::Object, span)
4755+
call(
4756+
&format!("zb_dict_get_default{by}"),
4757+
vec![d, k, default],
4758+
Ty::Object,
4759+
span,
4760+
)
47204761
}
47214762
("setdefault", 2) => {
47224763
let k = self.expr_as(&args[0], Ty::Object)?;

0 commit comments

Comments
 (0)