diff --git a/llvmlite/ir/_utils.py b/llvmlite/ir/_utils.py index 8287d77af..9b23e12eb 100644 --- a/llvmlite/ir/_utils.py +++ b/llvmlite/ir/_utils.py @@ -1,4 +1,3 @@ -from collections import defaultdict class DuplicatedNameError(NameError): @@ -8,7 +7,7 @@ class DuplicatedNameError(NameError): class NameScope(object): def __init__(self): self._useset = set(['']) - self._basenamemap = defaultdict(int) + self._basenamemap = {} def is_used(self, name): return name in self._useset @@ -23,10 +22,18 @@ def register(self, name, deduplicate=False): def deduplicate(self, name): basename = name + + try: + ident = self._basenamemap[basename] + except KeyError: + ident = 0 + while self.is_used(name): - ident = self._basenamemap[basename] + 1 - self._basenamemap[basename] = ident + ident += 1 name = "{0}.{1}".format(basename, ident) + + self._basenamemap[basename] = ident + return name def get_child(self): diff --git a/llvmlite/ir/builder.py b/llvmlite/ir/builder.py index e648869da..a9c126823 100644 --- a/llvmlite/ir/builder.py +++ b/llvmlite/ir/builder.py @@ -119,8 +119,8 @@ def wrapped(self, operand, flag, name=''): raise TypeError( "expected an integer type, got %s" % operand.type) - if not(isinstance(flag.type, types.IntType) and - flag.type.width == 1): + if not (isinstance(flag.type, types.IntType) and + flag.type.width == 1): raise TypeError("expected an i1 type, got %s" % flag.type) fn = self.module.declare_intrinsic( opname, [operand.type, flag.type]) diff --git a/llvmlite/ir/module.py b/llvmlite/ir/module.py index 464f91ec3..48a858cf4 100644 --- a/llvmlite/ir/module.py +++ b/llvmlite/ir/module.py @@ -52,14 +52,17 @@ def add_metadata(self, operands): if not isinstance(operands, (list, tuple)): raise TypeError("expected a list or tuple of metadata values, " "got %r" % (operands,)) - operands = self._fix_metadata_operands(operands) - key = tuple(operands) - if key not in self._metadatacache: - n = len(self.metadata) - md = values.MDValue(self, operands, name=str(n)) - self._metadatacache[key] = md - else: - md = self._metadatacache[key] + + fixed_operands = self._fix_metadata_operands(operands) + key = hash(tuple(fixed_operands)) + try: + return self._metadatacache[key] + except KeyError: + pass + + n = len(self.metadata) + md = values.MDValue(self, fixed_operands, name=str(n)) + self._metadatacache[key] = md return md def add_debug_info(self, kind, operands, is_distinct=False): @@ -72,14 +75,18 @@ def add_debug_info(self, kind, operands, is_distinct=False): A DIValue instance is returned, it can then be associated to e.g. an instruction. """ - operands = tuple(sorted(self._fix_di_operands(operands.items()))) - key = (kind, operands, is_distinct) - if key not in self._metadatacache: - n = len(self.metadata) - di = values.DIValue(self, is_distinct, kind, operands, name=str(n)) - self._metadatacache[key] = di - else: - di = self._metadatacache[key] + fixed_operands = self._fix_di_operands(sorted(operands.items())) + key = hash((kind, tuple(fixed_operands), is_distinct)) + + try: + return self._metadatacache[key] + except KeyError: + pass + + n = len(self.metadata) + di = values.DIValue( + self, is_distinct, kind, fixed_operands, name=str(n)) + self._metadatacache[key] = di return di def add_named_metadata(self, name, element=None): diff --git a/llvmlite/ir/values.py b/llvmlite/ir/values.py index 853927724..6a9d40d48 100644 --- a/llvmlite/ir/values.py +++ b/llvmlite/ir/values.py @@ -667,6 +667,7 @@ def __init__(self, parent, values, name): types.MetaDataType(), name=name) self.operands = tuple(values) + self.hash_cache = None parent.metadata.append(self) def descr(self, buf): @@ -694,8 +695,17 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) + def __getstate__(self): + # Ensure that the hash is not cached between Python invocations + # due to pickling or other serialization. The hash seed changes + # which will cause these not to match. + self.hash_cache = None + return self.__dict__ + def __hash__(self): - return hash(self.operands) + if self.hash_cache is None: + self.hash_cache = hash(self.operands) + return self.hash_cache class DIToken: @@ -725,6 +735,7 @@ def __init__(self, parent, is_distinct, kind, operands, name): self.is_distinct = is_distinct self.kind = kind self.operands = tuple(operands) + self.hash_cache = None parent.metadata.append(self) def descr(self, buf): @@ -767,8 +778,17 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) + def __getstate__(self): + # Ensure that the hash is not cached between Python invocations + # due to pickling or other serialization. The hash seed changes + # which will cause these not to match. + self.hash_cache = None + return self.__dict__ + def __hash__(self): - return hash((self.is_distinct, self.kind, self.operands)) + if self.hash_cache is None: + self.hash_cache = hash(self.operands) + return self.hash_cache class GlobalValue(NamedValue, _ConstOpMixin, _HasMetadata): diff --git a/llvmlite/tests/test_ir.py b/llvmlite/tests/test_ir.py index 8516b1651..6560c5ffe 100644 --- a/llvmlite/tests/test_ir.py +++ b/llvmlite/tests/test_ir.py @@ -7,6 +7,7 @@ import pickle import re import textwrap +import timeit import unittest from . import TestCase @@ -526,6 +527,94 @@ def test_debug_info_unicode_string(self): name = ''.join(map(lambda x: f"\\{x:02x}", "∆".encode())) self.assert_ir_line(f'!0 = !DILocalVariable(name: "a{name}")', strmod) + def test_debug_info_caching(self): + mod = None + foo = None + builder = None + di_subprogram = None + + def setup_test(): + nonlocal mod, foo, builder, di_subprogram + mod = self.module() + + di_file = mod.add_debug_info( + 'DIFile', + { + 'directory': 'my_directory', + 'filename': 'my_path.foo', + }) + + di_compile_unit = mod.add_debug_info( + 'DICompileUnit', + { + 'emissionKind': ir.DIToken('FullDebug'), + 'file': di_file, + 'isOptimized': False, + 'language': ir.DIToken('DW_LANG_C99'), + 'producer': 'llvmlite-test', + }) + + di_subprogram = mod.add_debug_info( + 'DISubprogram', + { + 'file': di_file, + 'line': 123, + 'name': 'my function', + 'scope': di_file, + 'scopeLine': 123, + 'unit': di_compile_unit, + }) + + foo = ir.Function(mod, ir.FunctionType(ir.VoidType(), []), 'foo') + builder = ir.IRBuilder(foo.append_basic_block('')) + + def do_test(): + for i in range(100_000): + di_location = mod.add_debug_info( + 'DILocation', + { + 'scope': di_subprogram, + 'line': i, + 'column': 15, + 'other': [di_subprogram, di_subprogram], + }) + + builder.debug_metadata = di_location + + builder.and_( + ir.Constant(ir.IntType(bits=32), i), + ir.Constant(ir.IntType(bits=32), i + 1)) + + total_time = timeit.timeit( + 'do_test()', + 'setup_test()', + number=10, + globals=locals()) + + print('test_debug_info_performance took', total_time, 'to finish') + + self.assertEqual(100004, len(mod._metadatacache)) + + def test_debug_info_pickle(self): + mod = self.module() + + di_file = mod.add_debug_info( + 'DIFile', + { + 'directory': 'my_directory', + 'filename': 'my_path.foo', + }) + self.assertEqual(hash(di_file), di_file.hash_cache) + found_di_file = pickle.loads(pickle.dumps(di_file)) + self.assertIsNone(found_di_file.hash_cache) + self.assertEqual(di_file, found_di_file) + + meta = mod.add_metadata([di_file]) + self.assertEqual(hash(meta), meta.hash_cache) + found_meta = pickle.loads(pickle.dumps(meta)) + self.assertIsNone(found_meta.hash_cache) + self.assertEqual(meta, found_meta) + def test_inline_assembly(self): mod = self.module() foo = ir.Function(mod, ir.FunctionType(ir.VoidType(), []), 'foo')