609 lines
23 KiB
Python
609 lines
23 KiB
Python
# starch: framework glue for selecting ISA-specific code at runtime
|
|
|
|
# Copyright (c) 2020, FlightAware LLC.
|
|
# All rights reserved.
|
|
# See the LICENSE file for licensing terms.
|
|
|
|
import sys
|
|
import re
|
|
import os
|
|
import mako.lookup
|
|
from functools import total_ordering
|
|
|
|
from typing import Optional, Union, Iterable, Sequence, MutableSequence, Mapping, MutableMapping, FrozenSet, Dict, List
|
|
|
|
class Feature(object):
|
|
"""Feature represents a type of code that can only be built with
|
|
certain compiler flags. For example, code that uses NEON intrinsics
|
|
can only be compiled if the compiler is building for an ARM instruction
|
|
set that supports NEON. Implementation code should be conditionally
|
|
compiled using the corresponding macro name, and should declare
|
|
themselves using the STARCH_IMPL_REQUIRES macro."""
|
|
|
|
gen: 'Generator'
|
|
name: str
|
|
description: str
|
|
|
|
def __init__(self,
|
|
gen: 'Generator',
|
|
name: str,
|
|
description: str):
|
|
self.gen = gen
|
|
self.name = name
|
|
self.description = description
|
|
|
|
@property
|
|
def macro(self) -> str:
|
|
return 'STARCH_FEATURE_' + self.name.upper()
|
|
|
|
@total_ordering
|
|
class BuildFlavor(object):
|
|
"""BuildFlavor models code built with specific compiler flags.
|
|
Shared implementation code will be built multiple times, once per flavor.
|
|
|
|
Each flavor has an associated test function that is called at runtime to
|
|
check if the current hardware supports the code emitted by the flavor. If
|
|
the test function returns false, no code built with the flavor will be executed.
|
|
|
|
Each flavor has a (possibly empty) list of optional Features that may
|
|
be present at runtime. This list controls which feature-dependent code is
|
|
compiled for this flavor (e.g. an x86 flavor might try to build code that
|
|
depends on SSE, but should not try to build code that depends on ARM NEON
|
|
intrinsics)"""
|
|
|
|
gen: 'Generator'
|
|
name: str
|
|
description: str
|
|
compile_flags: Sequence[str]
|
|
features: FrozenSet[Feature]
|
|
test_function: Optional[str]
|
|
alignment: int
|
|
|
|
def __init__(self,
|
|
gen: 'Generator',
|
|
name: str,
|
|
description: str,
|
|
compile_flags: Iterable[str] = (),
|
|
features: Iterable[Feature] = (),
|
|
test_function: Optional[str] = None,
|
|
alignment: int = 1):
|
|
|
|
self.gen = gen
|
|
self.name = name
|
|
self.compile_flags = tuple(compile_flags)
|
|
self.features = frozenset(features)
|
|
self.test_function = test_function
|
|
self.alignment = alignment
|
|
|
|
@property
|
|
def macro(self) -> str:
|
|
return 'STARCH_FLAVOR_' + self.name.upper()
|
|
|
|
@property
|
|
def test_function_expr(self) -> str:
|
|
if self.test_function is None:
|
|
return "NULL"
|
|
else:
|
|
return self.test_function
|
|
|
|
@property
|
|
def cflags(self) -> str:
|
|
return ' '.join(self.compile_flags)
|
|
|
|
def __lt__(self, other: object) -> bool:
|
|
if not isinstance(other, BuildFlavor):
|
|
return NotImplemented
|
|
return self.name < other.name
|
|
|
|
|
|
@total_ordering
|
|
class Function(object):
|
|
"""A user-callable function that will be dispatched to
|
|
one of the many possible implementations based on runtime feature
|
|
support."""
|
|
|
|
gen: 'Generator'
|
|
name: str
|
|
returntype: str
|
|
argtypes: Sequence[str]
|
|
argnames: Sequence[str]
|
|
impls: Sequence['FunctionImpl']
|
|
benchmark: Optional['SourceFile'] = None
|
|
benchmark_verify: Optional['SourceFile'] = None
|
|
aligned: bool
|
|
aligned_pair: Optional['Function'] = None
|
|
|
|
def __init__(self,
|
|
gen: 'Generator',
|
|
name: str,
|
|
argtypes: Iterable[str],
|
|
returntype: str = 'void',
|
|
argnames: Optional[Iterable[str]] = None,
|
|
aligned: bool = False):
|
|
|
|
self.gen = gen
|
|
self.name = name
|
|
self.returntype = returntype
|
|
self.argtypes = tuple(argtypes)
|
|
self.aligned = aligned
|
|
self.impls = []
|
|
|
|
if argnames is None:
|
|
self.argnames = tuple( f'arg{n}' for n in range(len(self.argtypes)) )
|
|
else:
|
|
self.argnames = tuple(argnames)
|
|
if len(self.argnames) != len(self.argtypes):
|
|
raise ValueError('length of argnames must match length of argtypes')
|
|
|
|
@property
|
|
def declaration_arglist(self) -> str:
|
|
return ', '.join([f'{typename} {argname}' for typename, argname in zip(self.argtypes, self.argnames)])
|
|
|
|
@property
|
|
def named_arglist(self) -> str:
|
|
return ', '.join(self.argnames)
|
|
|
|
@property
|
|
def callable_symbol(self) -> str:
|
|
if self.gen.prefix_function_symbols:
|
|
return self.gen.sym(self.name)
|
|
else:
|
|
return self.name
|
|
|
|
@property
|
|
def select_symbol(self) -> str:
|
|
return self.gen.sym(self.name + '_select')
|
|
|
|
@property
|
|
def dispatcher_symbol(self) -> str:
|
|
return self.gen.sym(self.name + '_dispatch')
|
|
|
|
@property
|
|
def pointer_type(self) -> str:
|
|
return self.gen.sym(self.name + '_ptr')
|
|
|
|
@property
|
|
def regentry_type(self) -> str:
|
|
return self.gen.sym(self.name + '_regentry')
|
|
|
|
@property
|
|
def registry_symbol(self) -> str:
|
|
return self.gen.sym(self.name + '_registry')
|
|
|
|
@property
|
|
def set_wisdom_symbol(self) -> str:
|
|
return self.gen.sym(self.name + '_set_wisdom')
|
|
|
|
@property
|
|
def benchmark_symbol(self) -> str:
|
|
return self.gen.sym(self.name + '_benchmark')
|
|
|
|
@property
|
|
def benchmark_verify_symbol(self) -> str:
|
|
return self.gen.sym(self.name + '_benchmark_verify')
|
|
|
|
def __lt__(self, other: object) -> bool:
|
|
if not isinstance(other, Function):
|
|
return NotImplemented
|
|
return self.name < other.name
|
|
|
|
|
|
class FunctionImpl(object):
|
|
"""A possible implementation of a function, not built in any particular way yet."""
|
|
|
|
gen: 'Generator'
|
|
function: Function
|
|
name: str
|
|
feature: Optional[Feature]
|
|
source: 'SourceFile'
|
|
lineno: int
|
|
|
|
def __init__(self,
|
|
gen: 'Generator',
|
|
function: Function,
|
|
name: str,
|
|
feature: Optional[Feature],
|
|
source: 'SourceFile',
|
|
lineno: int):
|
|
self.gen = gen
|
|
self.function = function
|
|
self.name = name
|
|
self.feature = feature
|
|
self.source = source
|
|
self.lineno = lineno
|
|
|
|
def wisdom_name(self, flavor) -> str:
|
|
if self.function.aligned:
|
|
return self.name + '_' + flavor.name + '_aligned'
|
|
else:
|
|
return self.name + '_' + flavor.name
|
|
|
|
def impl_symbol(self, flavor) -> str:
|
|
return self.gen.sym(self.function.name + '_' + self.name + '_' + flavor.name)
|
|
|
|
|
|
@total_ordering
|
|
class SourceFile(object):
|
|
"""A scanned source file that contains implementation code."""
|
|
|
|
path: str
|
|
impls: Sequence[FunctionImpl]
|
|
|
|
def __init__(self, path):
|
|
self.path = path
|
|
self.impls = []
|
|
|
|
def __lt__(self, other: object) -> bool:
|
|
if not isinstance(other, SourceFile):
|
|
return NotImplemented
|
|
return self.path < other.path
|
|
|
|
|
|
@total_ordering
|
|
class BuildMix(object):
|
|
"""A combination of build flavors that make up one possible way of building all
|
|
the code. The output of a mix is a library that dispatches functions within the
|
|
mixed flavors. For example, when building a binary that is intended to run on
|
|
generic ARM systems, a mix could be used that includes flavors for ARMv6, ARMv7,
|
|
and ARMv8.
|
|
|
|
The order of flavors within a mix is significant. At runtime, flavors will be tried
|
|
in order until a supported flavor is found; so more efficient flavors should be
|
|
specified first."""
|
|
|
|
name: str
|
|
description: str
|
|
flavors: Sequence[BuildFlavor]
|
|
wisdom: Mapping[Function,Sequence[str]]
|
|
|
|
def __init__(self,
|
|
name: str,
|
|
description: str,
|
|
flavors: Iterable[BuildFlavor],
|
|
wisdom: Mapping[Function,Iterable[str]] = {}):
|
|
self.name = name
|
|
self.description = description
|
|
self.flavors = tuple(flavors)
|
|
self.wisdom = dict( (k,tuple(v)) for k, v in wisdom.items() )
|
|
|
|
@property
|
|
def macro(self):
|
|
return 'STARCH_MIX_' + self.name.upper()
|
|
|
|
def function_wisdom(self, function) -> Sequence[str]:
|
|
return self.wisdom.get(function, [])
|
|
|
|
def __lt__(self, other: object) -> bool:
|
|
if not isinstance(other, BuildMix):
|
|
return NotImplemented
|
|
return self.name < other.name
|
|
|
|
|
|
class Generator(object):
|
|
functions: MutableMapping[str, Function]
|
|
features: MutableMapping[str, Feature]
|
|
features_by_macro: MutableMapping[str, Feature]
|
|
flavors: MutableMapping[str, BuildFlavor]
|
|
function_impls: MutableMapping[str, FunctionImpl]
|
|
impl_files: MutableSequence[SourceFile]
|
|
benchmark_files: MutableSequence[SourceFile]
|
|
mixes: MutableMapping[str, BuildMix]
|
|
symbol_prefix: str
|
|
templates: mako.lookup.TemplateLookup
|
|
generated_include_path: str
|
|
generated_flavor_pattern: str
|
|
generated_dispatcher_path: str
|
|
generated_benchmark_path: str
|
|
generated_makefile_pattern: str
|
|
includes: MutableSequence[str] = []
|
|
|
|
def __init__(self,
|
|
runtime_dir: str,
|
|
output_dir: str,
|
|
template_dir: Optional[str] = None,
|
|
mako_dir: Optional[str] = None,
|
|
generated_include_path: str = 'starch.h',
|
|
generated_flavor_pattern: str = 'flavor.{0}.c',
|
|
generated_dispatcher_path: str = 'dispatcher.c',
|
|
generated_benchmark_path: str = 'benchmark.c',
|
|
generated_makefile_pattern: str = 'makefile.{0}',
|
|
symbol_prefix: str = 'starch_',
|
|
prefix_function_symbols: bool = True):
|
|
self.runtime_dir = runtime_dir
|
|
self.output_dir = output_dir
|
|
self.generated_include_path = os.path.join(output_dir, generated_include_path)
|
|
self.generated_flavor_pattern = generated_flavor_pattern
|
|
self.generated_dispatcher_path = os.path.join(output_dir, generated_dispatcher_path)
|
|
self.generated_benchmark_path = os.path.join(output_dir, generated_benchmark_path)
|
|
self.generated_makefile_pattern = generated_makefile_pattern
|
|
self.symbol_prefix = symbol_prefix
|
|
self.prefix_function_symbols = prefix_function_symbols
|
|
|
|
if template_dir is None and '__file__' in globals():
|
|
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
|
if template_dir is None:
|
|
raise RuntimeError('cannot determine template directory location, please specify template_dir')
|
|
self.templates = mako.lookup.TemplateLookup(directories = [template_dir], module_directory = mako_dir, imports=['import os'])
|
|
|
|
self.functions = {}
|
|
self.features = {}
|
|
self.features_by_macro = {}
|
|
self.flavors = {}
|
|
self.function_impls = {}
|
|
self.impl_files = []
|
|
self.benchmark_files = []
|
|
self.mixes = {}
|
|
self.includes = []
|
|
|
|
def generated_flavor_path(self, flavor: BuildFlavor) -> str:
|
|
return os.path.join(self.output_dir, self.generated_flavor_pattern.format(flavor.name))
|
|
|
|
def generated_makefile_path(self, mix: BuildMix) -> str:
|
|
return os.path.join(self.output_dir, self.generated_makefile_pattern.format(mix.name))
|
|
|
|
def add_include(self, what):
|
|
if what[0] == '<' or what[0] == '"':
|
|
self.includes.append(what)
|
|
else:
|
|
self.includes.append('"' + what + '"')
|
|
|
|
def add_feature(self,
|
|
name: str,
|
|
description: str):
|
|
if name in self.features:
|
|
raise RuntimeError('duplicated flavor: ' + name)
|
|
feature = Feature(self, name, description)
|
|
self.features[name] = self.features_by_macro[feature.macro] = feature
|
|
|
|
def get_feature(self, key: Union[str, Feature]) -> Feature:
|
|
if isinstance(key, Feature):
|
|
return key
|
|
return self.features[key]
|
|
|
|
def get_feature_macro(self, key: str) -> Optional[Feature]:
|
|
return self.features_by_macro.get(key, None)
|
|
|
|
def add_function(self,
|
|
name: str,
|
|
argtypes: Iterable[str],
|
|
returntype: str = 'void',
|
|
argnames: Optional[Iterable[str]] = None,
|
|
aligned: bool = False):
|
|
if name in self.functions:
|
|
raise RuntimeError('duplicated function: ' + name)
|
|
|
|
base_function = Function(self, name, argtypes, returntype, argnames, aligned = False)
|
|
aligned_function: Optional[Function] = None
|
|
if aligned:
|
|
aligned_function = Function(self, name + '_aligned', argtypes, returntype, argnames, aligned = True)
|
|
base_function.aligned_pair = aligned_function
|
|
aligned_function.aligned_pair = base_function
|
|
|
|
self.functions[base_function.name] = base_function
|
|
if aligned_function:
|
|
self.functions[aligned_function.name] = aligned_function
|
|
|
|
def get_function(self, key: Union[str, Function]) -> Function:
|
|
if isinstance(key, Function):
|
|
return key
|
|
return self.functions[key]
|
|
|
|
def add_flavor(self,
|
|
name: str,
|
|
description: str,
|
|
compile_flags: Iterable[str] = (),
|
|
features: Iterable[Union[Feature,str]] = (),
|
|
test_function: Optional[str] = None,
|
|
alignment: int = 1):
|
|
if name in self.flavors:
|
|
raise RuntimeError('duplicated flavor: ' + name)
|
|
resolved_features = map(self.get_feature, features)
|
|
self.flavors[name] = BuildFlavor(self, name, description, compile_flags, resolved_features, test_function, alignment)
|
|
|
|
def get_flavor(self, key: Union[str, BuildFlavor]) -> BuildFlavor:
|
|
if isinstance(key, BuildFlavor):
|
|
return key
|
|
return self.flavors[key]
|
|
|
|
def load_wisdom(self, path: str) -> Mapping[Function,Sequence[str]]:
|
|
results: Dict[Function,List[str]] = {}
|
|
|
|
try:
|
|
f = open(path, 'r')
|
|
except IOError:
|
|
self.warning(None, None, f"ignoring missing wisdom file {path}")
|
|
return results
|
|
|
|
with f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line == '' or line.startswith('#'):
|
|
continue
|
|
|
|
parts = re.split('\s+', line)
|
|
if len(parts) < 2:
|
|
continue
|
|
|
|
func, impl = parts[:2]
|
|
if func in self.functions:
|
|
results.setdefault(self.functions[func], []).append(impl)
|
|
else:
|
|
self.warning(None, None, f"ignoring unknown function {func} in wisdom file {path}")
|
|
|
|
return results
|
|
|
|
def add_mix(self,
|
|
name: str,
|
|
description: str,
|
|
flavors: Iterable[Union[BuildFlavor,str]],
|
|
wisdom: Mapping[Union[Function,str],Iterable[str]] = {},
|
|
wisdom_file: Optional[str] = None):
|
|
if name in self.mixes:
|
|
raise RuntimeError('duplicated mix: ' + name)
|
|
|
|
resolved_flavors = map(self.get_flavor, flavors)
|
|
if wisdom_file:
|
|
resolved_wisdom = self.load_wisdom(wisdom_file)
|
|
else:
|
|
resolved_wisdom = dict( (self.get_function(name), list(values)) for name,values in wisdom.items() )
|
|
self.mixes[name] = BuildMix(name, description, resolved_flavors, resolved_wisdom)
|
|
|
|
def sym(self, symbol: str) -> str:
|
|
return self.symbol_prefix + symbol
|
|
|
|
def build_impls(self, source: SourceFile, lineno: int, function_name: str, impl_name: str, feature_name: Optional[str] = None) -> Sequence[FunctionImpl]:
|
|
if function_name not in self.functions:
|
|
self.warning(source, lineno, f"implementation defined for unknown function '{function_name}', skipped")
|
|
return []
|
|
|
|
function = self.functions[function_name]
|
|
|
|
feature: Optional[Feature] = None
|
|
if feature_name is not None:
|
|
if feature_name not in self.features_by_macro:
|
|
self.warning(source, lineno, f"implementation {function_name} ({impl_name}) requires unknown feature '{feature_name}', skipped")
|
|
return []
|
|
feature = self.features_by_macro.get(feature_name)
|
|
|
|
result = [FunctionImpl(gen = self,
|
|
function = function,
|
|
name = impl_name,
|
|
source = source,
|
|
lineno = lineno,
|
|
feature = feature)]
|
|
|
|
if function.aligned_pair:
|
|
result.append(FunctionImpl(gen = self,
|
|
function = function.aligned_pair,
|
|
name = impl_name,
|
|
source = source,
|
|
lineno = lineno,
|
|
feature = feature))
|
|
|
|
return result
|
|
|
|
def add_impl(self, impl):
|
|
key = (impl.function, impl.name)
|
|
old = self.function_impls.get(key)
|
|
if old:
|
|
self.warning(impl.source, impl.lineno, f'duplicate definition of {impl.function.name} / {impl.name}, previously defined at {old.source.path}:{old.lineno}')
|
|
return
|
|
self.function_impls[key] = impl
|
|
impl.function.impls.append(impl)
|
|
impl.source.impls.append(impl)
|
|
|
|
def warning(self, source: Optional[SourceFile], lineno: Optional[int], message):
|
|
if source is not None:
|
|
if lineno is not None:
|
|
print(f'{source.path}:{lineno}: warning: {message}', file=sys.stderr)
|
|
else:
|
|
print(f'{source.path}: warning: {message}', file=sys.stderr)
|
|
else:
|
|
print(f'warning: {message}', file=sys.stderr)
|
|
|
|
def scan_file(self, path: str):
|
|
source = SourceFile(path)
|
|
|
|
match_impl = re.compile(r'''[^a-zA-Z0-9_]+ STARCH_IMPL \s* \( \s* # macro call
|
|
([a-zA-Z0-9_]+) \s* , \s* # function name
|
|
([a-zA-Z0-9_]+) \s* \) # implementation name
|
|
''', re.VERBOSE)
|
|
match_impl_requires = re.compile(r'''[^a-zA-Z0-9_]+ STARCH_IMPL_REQUIRES \s* \( \s* # macro call
|
|
([a-zA-Z0-9_]+) \s* , \s* # function name
|
|
([a-zA-Z0-9_]+) \s* , \s* # implementation name
|
|
([a-zA-Z0-9_]+) \s* \) # feature name
|
|
''', re.VERBOSE)
|
|
|
|
match_benchmark = re.compile(r'''[^a-zA-Z0-9_]+ STARCH_BENCHMARK \s* \( \s* # macro call
|
|
([a-zA-Z0-9_]+) \s* \) # function name
|
|
''', re.VERBOSE)
|
|
|
|
match_verify = re.compile(r'''[^a-zA-Z0-9_]+ STARCH_BENCHMARK_VERIFY \s* \( \s* # macro call
|
|
([a-zA-Z0-9_]+) \s* \) # function name
|
|
''', re.VERBOSE)
|
|
|
|
has_benchmark = has_impl = has_benchmark_verify = False
|
|
with open(path, 'r') as f:
|
|
for lineno, line in enumerate(f):
|
|
if line[0] == '#':
|
|
continue # ignore preprocessor lines
|
|
|
|
for match in match_impl.finditer(line):
|
|
for impl in self.build_impls(source, lineno, match.group(1), match.group(2)):
|
|
has_impl = True
|
|
self.add_impl(impl)
|
|
|
|
for match in match_impl_requires.finditer(line):
|
|
for impl in self.build_impls(source, lineno, match.group(1), match.group(2), match.group(3)):
|
|
has_impl = True
|
|
self.add_impl(impl)
|
|
|
|
for match in match_benchmark.finditer(line):
|
|
function_name = match.group(1)
|
|
if function_name in self.functions:
|
|
function = self.functions[function_name]
|
|
if function.benchmark:
|
|
self.warning(source, lineno, f"duplicate benchmark defined for unknown function {function_name}")
|
|
function.benchmark = source
|
|
if function.aligned_pair:
|
|
function.aligned_pair.benchmark = source
|
|
has_benchmark = True
|
|
else:
|
|
self.warning(source, lineno, f"benchmark defined for unknown function {function_name}, ignored")
|
|
|
|
for match in match_verify.finditer(line):
|
|
function_name = match.group(1)
|
|
if function_name in self.functions:
|
|
function = self.functions[function_name]
|
|
if function.benchmark_verify:
|
|
self.warning(source, lineno, f"duplicate benchmark verifier defined for unknown function {function_name}")
|
|
function.benchmark_verify = source
|
|
if function.aligned_pair:
|
|
function.aligned_pair.benchmark_verify = source
|
|
has_benchmark_verify = True
|
|
else:
|
|
self.warning(source, lineno, f"benchmark verifier defined for unknown function {function_name}, ignored")
|
|
|
|
if has_impl:
|
|
self.impl_files.append(source)
|
|
if has_benchmark or has_benchmark_verify:
|
|
self.benchmark_files.append(source)
|
|
|
|
def render(self, template_path, output_path, **kwargs):
|
|
t = self.templates.get_template(template_path)
|
|
result = t.render(gen=self, current_dir=os.path.dirname(output_path), **kwargs).replace('\r\n', '\n')
|
|
|
|
if os.path.exists(output_path):
|
|
with open(output_path, 'r') as f:
|
|
contents = f.read()
|
|
if contents == result:
|
|
print(f'unchanged: {output_path}', file=sys.stderr)
|
|
return
|
|
|
|
with open(output_path, 'w') as f:
|
|
f.write(result)
|
|
print(f' wrote: {output_path}', file=sys.stderr)
|
|
|
|
def generate(self):
|
|
if not self.functions:
|
|
self.warning(None, None, 'no functions defined')
|
|
if not self.flavors:
|
|
self.warning(None, None, 'no flavors defined')
|
|
if not self.mixes:
|
|
self.warning(None, None, 'no mixes defined')
|
|
for function in self.functions.values():
|
|
if not function.impls:
|
|
self.warning(None, None, f'no implementations of function {function.name} provided')
|
|
|
|
self.render('/starch.h.template', self.generated_include_path)
|
|
|
|
for name, flavor in self.flavors.items():
|
|
self.render('/flavor.c.template', self.generated_flavor_path(flavor), flavor=flavor)
|
|
|
|
self.render('/dispatcher.c.template', self.generated_dispatcher_path)
|
|
self.render('/benchmark.c.template', self.generated_benchmark_path)
|
|
|
|
for name, mix in self.mixes.items():
|
|
self.render('/makefile.template', self.generated_makefile_path(mix), mix=mix)
|
|
|