diff options
| author | Ulf Magnusson <ulfalizer@gmail.com> | 2017-11-01 02:02:45 +0100 |
|---|---|---|
| committer | Ulf Magnusson <ulfalizer@gmail.com> | 2017-11-01 06:04:10 +0100 |
| commit | 70a9eb0668b51934f8d79f0c037d65aeb440bcef (patch) | |
| tree | 11cd7c6bced43852446e37e7f18b4dc555f1debb | |
| parent | 49a8303ca6e73419a3e26b963f78c61c6ab26d3b (diff) | |
Switch to a much faster invalidation algorithm
_cached_vis is always calculated as a side effect of calculating any
other cached value, and so doubles as a flag for whether an item (symbol
or choice) has any cached values.
If _cached_vis is None for some item, it also indirectly means that no
other item can have any cached values that (actually) depend on the
item, because _cached_vis would have gotten calculated as a side effect
of calculating any such cached value. Therefore, it's safe to stop
recursive invalidation at an item that has _cached_vis None.
Huge speedup for the allyes/noconfig*.py scripts. allnoconfig_simpler.py
went from 2.671 seconds to 1.067 seconds.
The dependency selftests need to be updated too now that
_get_dependent() is gone. The kernel defconfig tests pass even if all
global invalidation is disabled at least (except for the few defconfigs
that don't set MODULES=y), and that's a huge invalidation test. Seems
pretty speedy too, even though there's some redundant work, so maybe
things could be simplified a bit...
| -rw-r--r-- | examples/allnoconfig.py | 4 | ||||
| -rw-r--r-- | examples/allnoconfig_simpler.py | 13 | ||||
| -rw-r--r-- | kconfiglib.py | 195 | ||||
| -rw-r--r-- | testsuite.py | 62 |
4 files changed, 131 insertions, 143 deletions
diff --git a/examples/allnoconfig.py b/examples/allnoconfig.py index be36b47..3b7fd31 100644 --- a/examples/allnoconfig.py +++ b/examples/allnoconfig.py @@ -1,8 +1,8 @@ # Works like 'make allnoconfig'. Verified by the test suite to generate # identical output to 'make allnoconfig' for all ARCHes. # -# See allnoconfig_simpler.py for a much simpler version. This version -# demonstrates some tree walking and value processing. +# See allnoconfig_simpler.py for a much simpler version. This more roundabout +# version demonstrates some tree walking and value processing. # # Usage: # diff --git a/examples/allnoconfig_simpler.py b/examples/allnoconfig_simpler.py index 59a1bd4..81e701c 100644 --- a/examples/allnoconfig_simpler.py +++ b/examples/allnoconfig_simpler.py @@ -5,19 +5,6 @@ # Usage: # # $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig_simpler.py -# -# Implementation/performance note -# =============================== -# -# Kconfiglib immediately invalidates (flags for recalculation) all (possibly) -# dependent symbols when a value is assigned to a symbol, which slows this down -# a bit (due to tons of redundant invalidation), but makes any assignment -# pattern safe ("just works"). Kconfig.load_config() instead invalidates all -# symbols up front, making it much faster. If you really need to eke out -# performance, look at how load_config() does things (which involves internal -# APIs that don't invalidate symbols). This has been fast enough for all cases -# I've seen so far though (around 3 seconds for this particular script on my -# Core i7 2600K, including the initial Kconfig parsing). from kconfiglib import Kconfig, BOOL, TRISTATE import sys diff --git a/kconfiglib.py b/kconfiglib.py index 27a8e7b..04e1493 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -581,7 +581,7 @@ class Kconfig(object): # Do various post-processing of the menu tree _finalize_tree(self.top_node) - # Build Symbol._direct_dependents for all symbols + # Build Symbol._dependents for all symbols self._build_dep() @property @@ -1801,58 +1801,69 @@ class Kconfig(object): def _build_dep(self): """ - Populates the Symbol._direct_dependents sets, which link symbols to the - symbols that immediately depend on them in the sense that changing the - value of the symbol might affect the values of the dependent symbols. - This is used for caching/invalidation. + Populates the Symbol/Choice._dependents sets, which contain all other + items (symbols and choices) that immediately depend on the item in the + sense that changing the value of the item might affect the value of the + dependent items. This is used for caching/invalidation. The calculated sets might be larger than necessary as we don't do any complex analysis of the expressions. """ - # The directly dependent symbols of a symbol S are: - # - # - Any symbols whose prompts, default values, rev_dep (select - # condition), weak_rev_dep (imply condition), or ranges depend on S - # - # - Any symbol that has S as a direct dependency (has S in - # direct_dep). This is needed to get invalidation right for - # 'imply'. - # - # - Any symbols that belong to the same choice statement as S. These - # won't be included in S._direct_dependents as it creates dependency - # loops, but S._get_dependent() includes them. - # - # - Any symbols in a choice statement that depends on S - - # Only calculate _direct_dependents for defined symbols. Constant and + # Only calculate _dependents for defined symbols. Constant and # undefined symbols could theoretically be selected/implied, but it - # shouldn't change their value, so it's not a true dependency. + # wouldn't change their value, so it's not a true dependency. for sym in self.defined_syms: + # Symbols depend on the following: + + # The prompt conditions for node in sym.nodes: if node.prompt is not None: _make_depend_on(sym, node.prompt[1]) + # The default values and their conditions for value, cond in sym.defaults: _make_depend_on(sym, value) _make_depend_on(sym, cond) + # The reverse and weak reverse dependencies _make_depend_on(sym, sym.rev_dep) _make_depend_on(sym, sym.weak_rev_dep) + # The ranges along with their conditions for low, high, cond in sym.ranges: _make_depend_on(sym, low) _make_depend_on(sym, high) _make_depend_on(sym, cond) + # The direct dependencies. This is usually redundant, as the direct + # dependencies get propagated to properties, but it's needed to get + # invalidation solid for 'imply', which only checks the direct + # dependencies (even if there are no properties to propagate it + # to). _make_depend_on(sym, sym.direct_dep) - if sym.choice is not None: - for node in sym.choice.nodes: - if node.prompt is not None: - _make_depend_on(sym, node.prompt[1]) + # In addition to the above, choice symbols depend on the choice + # they're in, but that's handled automatically since the Choice is + # propagated to the conditions of the properties before + # _build_dep() runs. + + for choice in self._choices: + # Choices depend on the following: + + # The prompt conditions + for node in choice.nodes: + if node.prompt is not None: + _make_depend_on(choice, node.prompt[1]) + + # The default symbol conditions + for _, cond in choice.defaults: + _make_depend_on(choice, cond) - for _, cond in sym.choice.defaults: - _make_depend_on(sym, cond) + # The choice symbols themselves, because the y mode selection might + # change if a choice symbol's visibility changes + for sym in choice.syms: + # the default selection depends on the symbols + sym._dependents.add(choice) def _invalidate_all(self): # Undefined symbols never change value and don't need to be @@ -2173,11 +2184,10 @@ class Symbol(object): __slots__ = ( "_already_written", "_cached_assignable", - "_cached_deps", "_cached_str_val", "_cached_tri_val", "_cached_vis", - "_direct_dependents", + "_dependents", "_write_to_conf", "choice", "defaults", @@ -2223,6 +2233,7 @@ class Symbol(object): return self._cached_str_val if self.orig_type in (BOOL, TRISTATE): + # Also calculates the visibility, so invalidation safe self._cached_str_val = TRI_TO_STR[self.tri_value] return self._cached_str_val @@ -2234,6 +2245,8 @@ class Symbol(object): return self.name val = "" + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) vis = self.visibility self._write_to_conf = (vis != 0) @@ -2331,6 +2344,8 @@ class Symbol(object): return self._cached_tri_val val = 0 + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) vis = self.visibility self._write_to_conf = (vis != 0) @@ -2589,16 +2604,13 @@ class Symbol(object): self.user_value = None - # Populated in Kconfig._build_dep() after parsing. Links the symbol to - # the symbols that immediately depend on it (in a caching/invalidation - # sense). The total set of dependent symbols for the symbol is - # calculated as needed in _get_dependent(). - self._direct_dependents = set() + # See Kconfig._build_dep() + self._dependents = set() # Cached values self._cached_str_val = self._cached_tri_val = self._cached_vis = \ - self._cached_deps = self._cached_assignable = None + self._cached_assignable = None # Flags @@ -2617,6 +2629,8 @@ class Symbol(object): if self.orig_type not in (BOOL, TRISTATE): return () + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) vis = self.visibility if not vis: @@ -2698,6 +2712,7 @@ class Symbol(object): if self.choice is not None and value == 2: self.choice.user_selection = self + self.choice._rec_invalidate() def _invalidate(self): """ @@ -2708,8 +2723,7 @@ class Symbol(object): def _rec_invalidate(self): """ - Invalidates the symbol and all symbols and choices that (possibly - indirectly) depend on it + Invalidates the symbol and all items that (possibly) depend on it. """ # Constant symbols must never be invalidated, because they lose their # value. They never appear as dependencies, but can still be manually @@ -2717,52 +2731,27 @@ class Symbol(object): if not self.is_constant: self._invalidate() - for item in self._get_dependent(): - item._invalidate() - - def _get_dependent(self): - """ - Returns the set of symbols that should be invalidated if the value of - the symbol changes, because they might be affected by the change. Note - that this is an internal API and probably of limited usefulness to - clients. - """ - if self._cached_deps is not None: - return self._cached_deps - - # Less readable version of the following, measured to reduce the the - # running time of _get_dependent() on kernel Kconfigs by about 1/3 as - # measured by line_profiler. - # - # res = set(self._direct_dependents) - # for s in self._direct_dependents: - # res |= s._get_dependent() - res = self._direct_dependents | \ - {sym for dep in self._direct_dependents - for sym in dep._get_dependent()} - - if self.choice is not None: - # Choices depend on their choice symbols - res.add(self.choice) - - # Choice symbols also depend (recursively) on their siblings. The - # siblings are not included in _direct_dependents to avoid - # dependency loops. - for sibling in self.choice.syms: - if sibling is not self: - res.add(sibling) - # Less readable version of the following: - # - # res |= sibling._direct_dependents - # for s in sibling._direct_dependents: - # res |= s._get_dependent() - res |= sibling._direct_dependents | \ - {sym for dep in sibling._direct_dependents - for sym in dep._get_dependent()} - - # The tuple conversion sped up allnoconfig_simpler.py by 10% - self._cached_deps = tuple(res) - return self._cached_deps + for item in self._dependents: + # _cached_vis doubles as a flag that tells us whether 'item' + # has cached values, because it's calculated as a side effect + # of calculating all other (non-constant) cached values. + # + # If item._cached_vis is None, it means there can't be cached + # values on other items that depend on 'item', because if there + # were, some value on 'item' would have been calculated and + # item._cached_vis set as a side effect. It's therefore safe to + # stop the invalidation at symbols with _cached_vis None. + # + # This approach massively speeds up scripts that set a lot of + # values, vs simply invalidating all possibly dependent symbols + # (even when you already have a list of all the dependent + # symbols, because some symbols get huge dependency trees). + # + # This gracefully handles dependency loops too, which is nice + # for choices, where the choice depends on the choice symbols + # and vice versa. + if item._cached_vis is not None: + item._rec_invalidate() class Choice(object): """ @@ -2903,7 +2892,7 @@ class Choice(object): "_cached_assignable", "_cached_selection", "_cached_vis", - "_direct_dependents", + "_dependents", "defaults", "is_constant", "is_optional", @@ -2950,6 +2939,8 @@ class Choice(object): if self.user_value is not None: val = max(val, self.user_value) + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) val = min(val, self.visibility) # Promote m to y for boolean choices @@ -3033,10 +3024,7 @@ class Choice(object): return self.user_value = value - - if self.syms: - # Hackish way to invalidate the choice and all the choice symbols - self.syms[0]._rec_invalidate() + self._rec_invalidate() def unset_value(self): """ @@ -3044,9 +3032,7 @@ class Choice(object): the user had never touched the mode or any of the choice symbols. """ self.user_value = self.user_selection = None - if self.syms: - # Hackish way to invalidate the choice and all the choice symbols - self.syms[0]._rec_invalidate() + self._rec_invalidate() def __repr__(self): """ @@ -3124,9 +3110,11 @@ class Choice(object): self.user_value = self.user_selection = None - # TODO + # Checked by _make_depend_on(). Just set it to avoid having to + # special-case choices. self.is_constant = False - self._direct_dependents = set() + # See Kconfig._build_dep() + self._dependents = set() # The prompts and default values without any dependencies from # enclosing menus and ifs propagated @@ -3142,7 +3130,8 @@ class Choice(object): """ Worker function for the 'assignable' attribute. """ - + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) vis = self.visibility if not vis: @@ -3161,6 +3150,16 @@ class Choice(object): self._cached_vis = self._cached_assignable = None self._cached_selection = _NO_CACHED_SELECTION + def _rec_invalidate(self): + """ + See Symbol._rec_invalidate() + """ + self._invalidate() + + for item in self._dependents: + if item._cached_vis is not None: + item._rec_invalidate() + class MenuNode(object): """ Represents a menu node in the configuration. This corresponds to an entry @@ -3513,7 +3512,7 @@ def _make_depend_on(sym, expr): """ if not isinstance(expr, tuple): if not expr.is_constant: - expr._direct_dependents.add(sym) + expr._dependents.add(sym) elif expr[0] in (AND, OR): _make_depend_on(sym, expr[1]) @@ -3524,9 +3523,9 @@ def _make_depend_on(sym, expr): elif expr[0] in _RELATIONS: if not expr[1].is_constant: - expr[1]._direct_dependents.add(sym) + expr[1]._dependents.add(sym) if not expr[2].is_constant: - expr[2]._direct_dependents.add(sym) + expr[2]._dependents.add(sym) else: _internal_error("Internal error while fetching symbols from an " diff --git a/testsuite.py b/testsuite.py index 9ebcef7..4cfc245 100644 --- a/testsuite.py +++ b/testsuite.py @@ -1700,39 +1700,41 @@ g c = Kconfig("Kconfiglib/tests/Kdep") - def verify_dependent(sym_name, deps_names): - sym = c.syms[sym_name] - deps = [c.syms[name] for name in deps_names] - sym_deps = sym._get_dependent() - verify(len(sym_deps) == len(set(sym_deps)), - "{}'s dependencies contains duplicates".format(sym_name)) - sym_deps = [item for item in sym_deps - if not isinstance(item, Choice)] - verify(len(sym_deps) == len(deps), - "Wrong number of dependent symbols for {}".format(sym_name)) - for dep in deps: - verify(dep in sym_deps, "{} should depend on {}". - format(dep.name, sym_name)) - - # Test twice to cover dependency caching - for i in range(0, 2): - n_deps = 39 - # Verify that D1, D2, .., D<n_deps> are dependent on D - verify_dependent("D", ("D{}".format(i) for i in range(1, n_deps + 1))) - # Choices - verify_dependent("A", ("B", "C")) - verify_dependent("B", ("A", "C")) - verify_dependent("C", ("A", "B")) - verify_dependent("S", ("A", "B", "C")) + # TODO: reintroduce these somehow + + #def verify_dependent(sym_name, deps_names): + # sym = c.syms[sym_name] + # deps = [c.syms[name] for name in deps_names] + # sym_deps = sym._get_dependent() + # verify(len(sym_deps) == len(set(sym_deps)), + # "{}'s dependencies contains duplicates".format(sym_name)) + # sym_deps = [item for item in sym_deps + # if not isinstance(item, Choice)] + # verify(len(sym_deps) == len(deps), + # "Wrong number of dependent symbols for {}".format(sym_name)) + # for dep in deps: + # verify(dep in sym_deps, "{} should depend on {}". + # format(dep.name, sym_name)) + + ## Test twice to cover dependency caching + #for i in range(0, 2): + # n_deps = 39 + # # Verify that D1, D2, .., D<n_deps> are dependent on D + # verify_dependent("D", ("D{}".format(i) for i in range(1, n_deps + 1))) + # # Choices + # verify_dependent("A", ("B", "C")) + # verify_dependent("B", ("A", "C")) + # verify_dependent("C", ("A", "B")) + # verify_dependent("S", ("A", "B", "C")) # Verify that the last symbol depends on the first in a long chain of # dependencies. Test twice to cover dependency caching. - c = Kconfig("Kconfiglib/tests/Kchain") + #c = Kconfig("Kconfiglib/tests/Kchain") - for i in range(0, 2): - verify(c.syms["CHAIN_26"] in c.syms["CHAIN_1"]._get_dependent(), - "Dependency chain broken") + #for i in range(0, 2): + # verify(c.syms["CHAIN_26"] in c.syms["CHAIN_1"]._get_dependent(), + # "Dependency chain broken") print("Testing compatibility with weird selects/implies...") @@ -1920,7 +1922,7 @@ def test_sanity(conf, arch): verify(sym not in conf.const_syms, sym.name + " in both 'syms' and 'const_syms'") - for dep in sym._direct_dependents: + for dep in sym._dependents: verify(not dep.is_constant, "the constant symbol {} depends on {}" .format(dep.name, sym.name)) @@ -1956,7 +1958,7 @@ def test_sanity(conf, arch): verify(not sym.nodes, '"{}" is constant but has menu nodes'.format(sym.name)) - verify(not sym._direct_dependents, + verify(not sym._dependents, '"{}" is constant but is a dependency of some symbol' .format(sym.name)) |
