Skip to content

Commit 0d51efa

Browse files
authored
Linear scanning in CompileUnit.iter_DIEs() (#625)
CompileUnit.iter_DIEs() is unnecessarily convoluted in terms of parse logic. Its purpose it to return the DIEs as they appear in the info section, but instead it performs recursive tree traversal via CompileUnit.iter_DIE_children(), which contains the more complicated logic of going over immediate children of a DIE. On a siblingless DIE tree (which is rare in practice but allowed by the format), this will contain extra work of parsing the whole subtree only to throw away the children of children. Physically, DIEs are stored linearly. Logically, they are a tree. iter_DIEs() is expected to return a flat collection, but it does so by building a tree (out of a flat collection) and then flattening it. This PR changes iter_DIEs() to parse sequentially instead. As a matter of performance, it shaves 5-7% from the linear scan time using iter_DIEs(). Tested on one rather artificial example; the speedup on real life DWARF might vary. Does not fix #623, but somewhat helps them. As a downside, it duplicates the caching and the tree building logic already present in CompileUnit.iter_DIE_children().
1 parent ecbd17e commit 0d51efa

2 files changed

Lines changed: 91 additions & 1 deletion

File tree

elftools/dwarf/compileunit.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,42 @@ def iter_DIEs(self):
133133
""" Iterate over all the DIEs in the CU, in order of their appearance.
134134
Note that null DIEs will also be returned.
135135
"""
136-
return self._iter_DIE_subtree(self.get_top_DIE())
136+
stm = self.dwarfinfo.debug_info_sec.stream
137+
pos = self.cu_die_offset
138+
end_pos = self.cu_offset + self.size
139+
140+
die = self.get_top_DIE()
141+
yield die
142+
pos += die.size
143+
parent = die
144+
i = 1
145+
while pos < end_pos:
146+
if i < len(self._diemap) and self._diemap[i] == pos: # DIE already cached
147+
die = self._dielist[i]
148+
else:
149+
die = DIE(self, stm, pos)
150+
self._dielist.insert(i, die)
151+
self._diemap.insert(i, pos)
152+
i += 1
153+
154+
die._parent = parent
155+
156+
if die.tag is None:
157+
parent._terminator = die
158+
parent = parent._parent
159+
160+
if die.has_children:
161+
parent = die
162+
163+
if die.tag == 'DW_TAG_imported_unit' and self.dwarfinfo.supplementary_dwarfinfo:
164+
# Falls back to subtree traversal in the supplemental DWARF. Any way to streamline that too?
165+
supp_die = die.get_DIE_from_attribute('DW_AT_import')
166+
yield from supp_die.cu._iter_DIE_subtree(supp_die)
167+
else:
168+
yield die
169+
170+
pos += die.size
171+
137172

138173
def iter_DIE_children(self, die):
139174
""" Given a DIE, yields either its children, without null DIE list

test/test_tree.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#------------------------------------------------------------------------------
2+
# elftools tests
3+
#
4+
# Seva Alekseyev (sevaa@sprynet.com)
5+
# This code is in the public domain
6+
#
7+
# Making sure the two ways of iterating through DIEs (linear and by tree)
8+
# return an identically built tree (parents, children, terminators).
9+
#
10+
# TODO: find a siblingless file. None in the corpus so far.
11+
#------------------------------------------------------------------------------
12+
13+
import unittest
14+
import os
15+
from elftools.elf.elffile import ELFFile
16+
17+
class TestTree(unittest.TestCase):
18+
def test_tree(self):
19+
self.run_test_on('dwarf_llpair.elf', 0, True)
20+
self.run_test_on('test_debugsup1.debug', 2, False)
21+
22+
def run_test_on(self, file_name, cu_index, test_cached):
23+
def die_summary(die):
24+
return (die.offset, die.tag, die._terminator.offset if die._terminator else None, die.get_parent().offset if die.get_parent() else None)
25+
26+
with ELFFile.load_from_path(os.path.join('test', 'testfiles_for_unittests', file_name)) as elf:
27+
di = elf.get_dwarf_info()
28+
cu = next(c for i,c in enumerate(di.iter_CUs()) if i == cu_index)
29+
#_terminator is only set on a DIE *after* that DIE is yielded during enumeration
30+
DIEs = [d for d in cu.iter_DIEs()]
31+
seq_DIEs = [die_summary(d) for d in DIEs]
32+
33+
if test_cached:
34+
sample_offset = DIEs[len(DIEs) // 2].offset
35+
# Offset of a random DIE from the middle for later
36+
37+
# Deliberately erase the CU/DIE cache to force a repeat parse - this time using an explicit tree traversal
38+
di = elf.get_dwarf_info()
39+
cu = next(c for i,c in enumerate(di.iter_CUs()) if i == cu_index)
40+
DIEs = [d for d in cu._iter_DIE_subtree(cu.get_top_DIE())]
41+
42+
tree_DIEs = [die_summary(d) for d in DIEs]
43+
self.assertSequenceEqual(seq_DIEs, tree_DIEs)
44+
45+
if test_cached:
46+
# Another repeat parse, with a nonblank cache
47+
di = elf.get_dwarf_info()
48+
cu = next(c for i,c in enumerate(di.iter_CUs()) if i == cu_index)
49+
cu.get_DIE_from_refaddr(sample_offset) # Cache this random DIE in the middle
50+
DIEs = [d for d in cu.iter_DIEs()]
51+
seq_DIEs_x = [die_summary(d) for d in DIEs]
52+
self.assertSequenceEqual(seq_DIEs, seq_DIEs_x)
53+
54+
if __name__ == '__main__':
55+
unittest.main()

0 commit comments

Comments
 (0)