-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstarch.py
608 lines (501 loc) · 23.2 KB
/
starch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
# 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)