#!/usr/bin/env python3
import os
import sys
import stat
import zipfile
import re
from io import BytesIO
import tempfile
import shutil
import platform

import requests

class TemporaryDirectory:
    def __enter__(self):
        self.tempdir = tempfile.mkdtemp()
        return self.tempdir

    def __exit__(self, type_, value, traceback):
        shutil.rmtree(self.tempdir)

def get_os():
    os_map = {'Darwin': 'mac', 'Linux': 'lin64'}
    system = platform.system()
    assert system in os_map, "Unsupported operating system (platform.system() should return " \
                             "Linux or Darwin)."
    return os_map[system]

def serach_path():
    """Return potential locations of IACA installation."""
    operating_system = get_os()
    # 1st choice: in ~/.kerncraft/iaca-{}
    # 2nd choice: in package directory / iaca-{}
    return [os.path.expanduser("~/.kerncraft/iaca/{}/".format(operating_system)),
            os.path.abspath(os.path.dirname(os.path.realpath(__file__))) + '/iaca/{}/'.format(
                operating_system)]

def find_iaca():
    """Return (hopefully) valid installation of IACA."""
    requires = ['iaca2.2', 'iaca2.3', 'iaca3.0']
    for path in serach_path():
        path += 'bin/'
        valid = True
        for r in requires:
            if not os.path.exists(path + r):
                valid = False
                break
        if valid:
            return path
    raise RuntimeError("No IACA installation found in {}. Run iaca_get command to fix this issue."
                       "".format(serach_path()))

def main():
    try:
        path = find_iaca()
        print('IACA already installed at', path)
        if '--force' in sys.argv:
            sys.argv.remove('--force')
        else:
            print('For forced installation add --force')
            sys.exit()
    except RuntimeError:
        pass
    if len(sys.argv) < 2 or sys.argv[1] != \
            "--I-accept-the-Intel-What-If-Pre-Release-License-Agreement-and-please-take-my-soul":
        print("Go to https://software.intel.com/protected-download/267266/157552 and read the"
              "Intel Pre-Release License Agreement.")
        print("")
        print("Add "
              "--I-accept-the-Intel-What-If-Pre-Release-License-Agreement-and-please-take-my-soul"
              " for installation of IACA.")
        sys.exit(1)

    if len(sys.argv) >= 3:
        assert sys.argv[2] in ['lin64', 'mac']
        operating_system = sys.argv[2]
    else:
        operating_system = get_os()

    # Locate and create IACA base directory, in reverse server order
    base_dir = None
    for path in reversed(serach_path()):
        print("Trying " + path + ": ", end='', file=sys.stderr)
        try:
            os.makedirs(path)
            base_dir = path
            break
        except PermissionError:
            # Continue trying with next location
            print("permission denied.", file=sys.stderr)
            continue
        except OSError:
            # Directory already exists
            print("already exists.", file=sys.stderr)
            continue
    if base_dir is None:
        print('Aborted.', file=sys.stderr)
        sys.exit(1)
    else:
        print("selected.", file=sys.stderr)

    URL = "https://software.intel.com/protected-download/267266/157552"

    s = requests.Session()
    r = s.get(URL)
    response_data = {
        'accept_license': 1,
        'form_build_id': re.search(r'name="form_build_id" value="([^"]+)" />', r.text).group(1),
        'form_id': 'intel_licensed_dls_step_1'}
    download_list = s.post(URL, data=response_data).text

    print("IACA v2.1 (for manual use - only version analyzing latency):", file=sys.stderr)
    if operating_system == 'mac':
        operating_system_temp = 'mac64'
    else:
        operating_system_temp = operating_system
    download_url = re.search(
        r'"(https://software.intel.com/[^"]*iaca-' + operating_system_temp + '\.zip)"',
        download_list).group(1)
    print("Downloading", download_url, "...", file=sys.stderr)
    r = s.get(download_url, stream=True)
    zfile = zipfile.ZipFile(BytesIO(r.content))
    members = [n
               for n in zfile.namelist()
               if '/.' not in n and n.startswith('iaca-{:}/'.format(operating_system_temp))]
    # Exctract to temp folder and copy to correct directory
    print("Extracting...", file=sys.stderr)
    with TemporaryDirectory() as tempdir:
        zfile.extractall(tempdir, members=members)
        shutil.copytree(tempdir + '/iaca-{}'.format(operating_system_temp), base_dir + 'v2.1')
    # Correct permissions of executables
    print("Correcting permissions of binary...")
    st = os.stat(base_dir + 'v2.1/bin/iaca')
    os.chmod(
        base_dir + 'v2.1/bin/iaca',
        st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
    )
    st = os.stat(base_dir + 'v2.1/bin/iaca.sh')
    os.chmod(
        base_dir + 'v2.1/bin/iaca.sh',
        st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
    )
    # Fix iaca.sh
    print("Fixing iaca.sh...", file=sys.stderr)
    iaca_sh = open(base_dir + 'v2.1/bin/iaca.sh').read()
    iaca_sh = iaca_sh.replace('realpath', 'readlink -f', 1)
    iaca_sh = iaca_sh.replace('mypath=`pwd`', 'mypath=`dirname $0`', 1)
    iaca_sh = iaca_sh.replace('path=$(cd "$(dirname "$0")"; pwd)',
                              'script=`readlink -f $0`\n\tpath=`dirname "$script"`', 1)
    open(base_dir + 'v2.1/bin/iaca.sh', 'w').write(iaca_sh)
    print("IACA v2.1 installed to", os.getcwd() + '/' + base_dir + 'v2.1', file=sys.stderr)

    print("IACA v2.2 (for NHM and WSM support):", file=sys.stderr)
    download_url = re.search(
        r'"(https://software.intel.com/[^"]*iaca-version-2.2-' + operating_system + '\.zip)"',
        download_list).group(1)
    print("Downloading", download_url, "...", file=sys.stderr)
    r = s.get(download_url, stream=True)
    zfile = zipfile.ZipFile(BytesIO(r.content))
    members = [n
               for n in zfile.namelist()
               if '/.' not in n and n.startswith('iaca-{:}/'.format(operating_system))]
    # Exctract to temp folder and copy to correct directory
    print("Extracting...", file=sys.stderr)
    with TemporaryDirectory() as tempdir:
        zfile.extractall(tempdir, members=members)
        shutil.copytree(tempdir + '/iaca-{}'.format(operating_system), base_dir + 'v2.2')
    # Correct permissions of executables
    print("Correcting permissions of binary...")
    st = os.stat(base_dir + 'v2.2/bin/iaca')
    os.chmod(
        base_dir + 'v2.2/bin/iaca',
        st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
    )
    st = os.stat(base_dir + 'v2.2/bin/iaca.sh')
    os.chmod(
        base_dir + 'v2.2/bin/iaca.sh',
        st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
    )
    # Fix iaca.sh
    print("Fixing iaca.sh...", file=sys.stderr)
    iaca_sh = open(base_dir + 'v2.2/bin/iaca.sh').read()
    iaca_sh = iaca_sh.replace('realpath', 'readlink -f', 1)
    iaca_sh = iaca_sh.replace('mypath=`pwd`', 'mypath=`dirname $0`', 1)
    iaca_sh = iaca_sh.replace('path=$(cd "$(dirname "$0")"; pwd)',
                              'script=`readlink -f $0`\n\tpath=`dirname "$script"`', 1)
    open(base_dir + 'v2.2/bin/iaca.sh', 'w').write(iaca_sh)
    print("IACA v2.2 installed to", os.getcwd() + '/' + base_dir + 'v2.2', file=sys.stderr)

    print("IACA v2.3 (for SNB and IVY support):", file=sys.stderr)
    download_url = re.search(
        r'"(https://software.intel.com/[^"]*iaca-version-2.3-' + operating_system + '\.zip)"',
        download_list).group(1)
    print("Downloading", download_url, "...", file=sys.stderr)
    r = s.get(download_url, stream=True)
    print("Reading zip file...", file=sys.stderr)
    zfile = zipfile.ZipFile(BytesIO(r.content))
    members = [n
               for n in zfile.namelist()
               if '/.' not in n and n.startswith('iaca-{:}/'.format(operating_system))]
    # Exctract to temp folder and copy to correct directory
    print("Extracting...", file=sys.stderr)
    with TemporaryDirectory() as tempdir:
        zfile.extractall(tempdir, members=members)
        shutil.copytree(tempdir + '/iaca-{}'.format(operating_system), base_dir + 'v2.3')
    # Correct permissions of executables
    print("Correcting permissions of binary...")
    st = os.stat(base_dir + 'v2.3/bin/iaca')
    os.chmod(
        base_dir + 'v2.3/bin/iaca',
        st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
    )
    st = os.stat(base_dir + 'v2.3/bin/iaca.sh')
    os.chmod(
        base_dir + 'v2.3/bin/iaca.sh',
        st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
    )
    # Fix iaca.sh
    print("Fixing iaca.sh...", file=sys.stderr)
    iaca_sh = open(base_dir + 'v2.3/bin/iaca.sh').read()
    iaca_sh = iaca_sh.replace('realpath', 'readlink -f', 1)
    iaca_sh = iaca_sh.replace('mypath=`pwd`', 'mypath=`dirname $0`', 1)
    iaca_sh = iaca_sh.replace('path=$(cd "$(dirname "$0")"; pwd)',
                              'script=`readlink -f $0`\n\tpath=`dirname "$script"`', 1)
    open(base_dir + 'v2.3/bin/iaca.sh', 'w').write(iaca_sh)
    print("IACA v2.3 installed to", os.getcwd() + '/' + base_dir + 'v2.3', file=sys.stderr)

    print("IACA v3.0 (for HSW, BDW, SKL and SKX support):", file=sys.stderr)
    download_url = re.search(
        r'"(https://software.intel.com/[^"]*iaca-version-v3.0-' + operating_system + '\.zip)"',
        download_list).group(1)
    print("Downloading...", download_url, "...", file=sys.stderr)
    r = s.get(download_url, stream=True)
    print("Reading zip file...", file=sys.stderr)
    zfile = zipfile.ZipFile(BytesIO(r.content))
    members = [n
               for n in zfile.namelist()
               if '/.' not in n and n.startswith('iaca-{:}/'.format(operating_system))]
    # Exctract to temp folder and copy to correct directory
    print("Extracting...", file=sys.stderr)
    with TemporaryDirectory() as tempdir:
        zfile.extractall(tempdir, members=members)
        shutil.copytree(tempdir + '/iaca-{}'.format(operating_system), base_dir + 'v3.0')

    print("Correcting permissions of binary...", file=sys.stderr)
    st = os.stat(base_dir + 'v3.0/iaca')
    os.chmod(
        base_dir + 'v3.0/iaca',
        st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
    )
    print("IACA v3.0 installed to", os.getcwd() + '/' + base_dir + 'v3.0', file=sys.stderr)

    # Create unified bin directory to access both operating_systems
    os.mkdir(base_dir + 'bin')
    os.symlink('../v2.1/bin/iaca.sh', base_dir + 'bin/iaca2.1')
    os.symlink('../v2.2/bin/iaca.sh', base_dir + 'bin/iaca2.2')
    os.symlink('../v2.3/bin/iaca.sh', base_dir + 'bin/iaca2.3')
    os.symlink('../v3.0/iaca', base_dir + 'bin/iaca3.0')
    print('export PATH=' + base_dir + 'bin/:$PATH')

if __name__ == '__main__':
    main()


#!/usr/bin/env python3
from pprint import pprint
import re
import string

def group_iterator(group):
    """
    Yild all groups of simple regex-like expression.

    The only special character is a dash (-), which take the preceding and the following chars to
    compute a range. If the range is non-sensical (e.g., b-a) it will be empty

    Example:
    >>> list(group_iterator('a-f'))
    ['a', 'b', 'c', 'd', 'e', 'f']
    >>> list(group_iterator('148'))
    ['1', '4', '8']
    >>> list(group_iterator('7-9ab'))
    ['7', '8', '9', 'a', 'b']
    >>> list(group_iterator('0B-A1'))
    ['0', '1']
    """
    ordered_chars = string.ascii_letters + string.digits
    tokenizer = ('(?P<seq>[a-zA-Z0-9]-[a-zA-Z0-9])|'
                 '(?P<chr>.)')
    for m in re.finditer(tokenizer, group):
        if m.group('seq'):
            start, sep, end = m.group('seq')
            for i in range(ordered_chars.index(start), ordered_chars.index(end)+1):
                yield ordered_chars[i]
        else:
            yield m.group('chr')

def register_options(regdescr):
    """
    Very reduced regular expressions for describing a group of registers.

    Only groups in square bracktes and unions with pipes (|) are supported.

    Examples:
    >>> list(register_options('PMC[0-3]'))
    ['PMC0', 'PMC1', 'PMC2', 'PMC3']
    >>> list(register_options('MBOX0C[01]'))
    ['MBOX0C0', 'MBOX0C1']
    >>> list(register_options('CBOX2C1'))
    ['CBOX2C1']
    >>> list(register_options('CBOX[0-3]C[01]'))
    ['CBOX0C0', 'CBOX0C1', 'CBOX1C0', 'CBOX1C1', 'CBOX2C0', 'CBOX2C1', 'CBOX3C0', 'CBOX3C1']
    >>> list(register_options('PMC[0-1]|PMC[23]'))
    ['PMC0', 'PMC1', 'PMC2', 'PMC3']

    """
    if not regdescr:
        yield None
    tokenizer = ('\[(?P<grp>[^]]+)\]|'
                 '(?P<chr>.)')
    for u in regdescr.split('|'):
        m = re.match(tokenizer, u)

        if m.group('grp'):
            current = group_iterator(m.group('grp'))
        else:
            current = [m.group('chr')]

        for c in current:
            if u[m.end():]:
                for r in register_options(u[m.end():]):
                    yield c + r
            else:
                yield c

def eventstr(event_tuple=None, event=None, register=None, parameters=None):
    """
    Return a LIKWID event string from an event tuple or keyword arguments.

    *event_tuple* may have two or three arguments: (event, register) or
    (event, register, parameters)

    Keyword arguments will be overwritten by *event_tuple*.

    >>> eventstr(('L1D_REPLACEMENT', 'PMC0', None))
    'L1D_REPLACEMENT:PMC0'
    >>> eventstr(('L1D_REPLACEMENT', 'PMC0'))
    'L1D_REPLACEMENT:PMC0'
    >>> eventstr(('MEM_UOPS_RETIRED_LOADS', 'PMC3', {'EDGEDETECT': None, 'THRESHOLD': 2342}))
    'MEM_UOPS_RETIRED_LOADS:PMC3:EDGEDETECT:THRESHOLD=0x926'
    >>> eventstr(event='DTLB_LOAD_MISSES_WALK_DURATION', register='PMC3')
    'DTLB_LOAD_MISSES_WALK_DURATION:PMC3'
    """
    if len(event_tuple) == 3:
        event, register, parameters = event_tuple
    elif len(event_tuple) == 2:
        event, register = event_tuple
    event_dscr = [event, register]

    if parameters:
        for k, v in sorted(event_tuple[2].items()):  # sorted for reproducability
            if type(v) is int:
                k += "={}".format(hex(v))
            event_dscr.append(k)
    return ":".join(event_dscr)

def build_minimal_runs(events):
    """Compile list of minimal runs for given events."""
    # Eliminate multiples
    events = [e for i, e in enumerate(events) if events.index(e) == i]

    # Build list of runs per register group
    scheduled_runs = {}
    scheduled_events = []
    cur_run = 0
    while len(scheduled_events) != len(events):
        for event_tpl in events:
            event, registers, parameters = event_tpl
            # Skip allready scheduled events
            if event_tpl in scheduled_events:
                continue
            # Compile explicit list of possible register locations
            for possible_reg in register_options(registers):
                # Schedule in current run, if register is not yet in use
                s = scheduled_runs.setdefault(cur_run, {})
                if possible_reg not in s:
                    s[possible_reg] = (event, possible_reg, parameters)
                    # ban from further scheduling attempts
                    scheduled_events.append(event_tpl)
                    break
        cur_run += 1

    # Collaps all register dicts to single runs
    runs = [list(v.values()) for v in scheduled_runs.values()]

    return runs

if __name__ == '__main__':
    requested = [
        ('L1D_REPLACEMENT', 'PMC[0-3]', None),
        ('BR_INST_RETIRED_ALL_BRANCHES', 'PMC[0-3]', None),
        ('BR_MISP_RETIRED_ALL_BRANCHES', 'PMC[0-3]', None),
        ('MEM_UOPS_RETIRED_LOADS', 'PMC[0-3]', None),
        ('MEM_UOPS_RETIRED_STORES', 'PMC[0-3]', None),
        ('DTLB_LOAD_MISSES_CAUSES_A_WALK', 'PMC[0-3]', None),
        ('DTLB_STORE_MISSES_CAUSES_A_WALK', 'PMC[0-3]', None),
        ('DTLB_LOAD_MISSES_WALK_DURATION', 'PMC[0-3]', None),
        ('DTLB_STORE_MISSES_WALK_DURATION', 'PMC[0-3]', None),
        ('CYCLE_ACTIVITY_CYCLES_L2_PENDING', 'PMC2', None),
        ('MEM_UOPS_RETIRED_LOADS', 'PMC[0-3]', {'EDGEDETECT': None}),
        ('MEM_UOPS_RETIRED_LOADS', 'PMC[0-3]', {'EDGEDETECT': None, 'THRESHOLD': 2342}),
        ('MEM_UOPS_RETIRED_LOADS', 'PMC[0-3]', {'EDGEDETECT': None, 'THRESHOLD': 0x926}),
        ('CPU_CLOCK_UNHALTED_THREAD_P', 'PMC[0-3]', {'THRESHOLD': 0x2, 'INVERT': None}),
        ('CAS_COUNT_RD', 'MBOX0C[01]', None),
        ('CAS_COUNT_WR', 'MBOX0C[01]', None),
        ('CAS_COUNT_RD', 'MBOX1C[01]', None),
        ('CAS_COUNT_WR', 'MBOX1C[01]', None),
        ('CAS_COUNT_RD', 'MBOX2C[01]', None),
        ('CAS_COUNT_WR', 'MBOX2C[01]', None),
        ('CAS_COUNT_RD', 'MBOX3C[01]', None),
        ('CAS_COUNT_WR', 'MBOX3C[01]', None),
        ('CAS_COUNT_RD', 'MBOX4C[01]', None),
        ('CAS_COUNT_WR', 'MBOX4C[01]', None),
        ('CAS_COUNT_RD', 'MBOX5C[01]', None),
        ('CAS_COUNT_WR', 'MBOX5C[01]', None),
        ('CAS_COUNT_RD', 'MBOX6C[01]', None),
        ('CAS_COUNT_WR', 'MBOX6C[01]', None),
        ('CAS_COUNT_RD', 'MBOX7C[01]', None),
        ('CAS_COUNT_WR', 'MBOX7C[01]', None),
        ('LLC_LOOKUP_ANY', 'CBOX0C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX1C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX2C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX3C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX4C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX5C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX6C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX7C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX8C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX9C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX10C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX11C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX12C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX13C1', {'STATE': 0x1}),
        ('LLC_LOOKUP_ANY', 'CBOX14C1', {'STATE': 0x1}),
    ]

    print(len(requested))
    runs = build_minimal_runs(requested)
    pprint([[eventstr(e) for e in r] for r in runs])
    print(len(runs))


#!/usr/bin/env python3
"""Roofline model and helper functions."""
import sys
from pprint import pformat  # Do not use pprint, breaks in combination with --store and StringIO

from kerncraft.prefixedunit import PrefixedUnit
from kerncraft.cacheprediction import LayerConditionPredictor, CacheSimulationPredictor
from .base import PerformanceModel

class Roofline(PerformanceModel):
    """
    Representation of the Roofline model based on simplistic FLOP analysis.

    more info to follow...
    """

    name = "Roofline"

    @classmethod
    def configure_arggroup(cls, parser):
        """Configure argument parser."""
        pass

    def __init__(self, kernel, machine, args=None, parser=None, cores=1,
                 cache_predictor=LayerConditionPredictor, verbose=0):
        """
        Create roofline model from kernel and machine objects.

        *kernel* is a Kernel object
        *machine* describes the machine (cpu, cache and memory) characteristics
        *args* (optional) are the parsed arguments from the comand line

        If *args* is None, *asm_block*, *pointer_increment* and *verbose* will be used, otherwise
        *args* takes precedence.
        """
        self.kernel = kernel
        self.machine = machine
        self._args = args
        self._parser = parser
        self.results = None

        if args:
            self.verbose = self._args.verbose
            self.cores = self._args.cores
            if self._args.cache_predictor == 'SIM':
                self.predictor = CacheSimulationPredictor(self.kernel, self.machine, self.cores)
            elif self._args.cache_predictor == 'LC':
                self.predictor = LayerConditionPredictor(self.kernel, self.machine, self.cores)
            else:
                raise NotImplementedError("Unknown cache predictor, only LC (layer condition) and "
                                          "SIM (cache simulation with pycachesim) is supported.")
        else:
            self.cores = cores
            self.predictor = cache_predictor(self.kernel, self.machine, self.cores)
            self.verbose = verbose

        if sum(self.kernel._flops.values()) == 0:
            raise ValueError("The Roofline model requires that the sum of FLOPs is non-zero.")

    def calculate_cache_access(self):
        """Apply cache prediction to generate cache access behaviour."""
        self.results = {'misses': self.predictor.get_misses(),
                        'hits': self.predictor.get_hits(),
                        'evicts': self.predictor.get_evicts(),
                        'verbose infos': self.predictor.get_infos(),  # only for verbose outputs
                        'bottleneck level': 0,
                        'mem bottlenecks': []}

        element_size = self.kernel.datatypes_size[self.kernel.datatype]
        cacheline_size = float(self.machine['cacheline size'])
        elements_per_cacheline = int(cacheline_size // element_size)

        total_flops = sum(self.kernel._flops.values())*elements_per_cacheline

        # TODO let user choose threads_per_core:
        threads_per_core = 1

        # Compile relevant information

        # CPU-L1 stats (in bytes!)
        # We compile CPU-L1 stats on our own, because cacheprediction only works on cache lines
        read_offsets, write_offsets = zip(*list(self.kernel.compile_global_offsets(
            iteration=range(0, elements_per_cacheline))))
        read_offsets = set([item for sublist in read_offsets if sublist is not None
                            for item in sublist])
        write_offsets = set([item for sublist in write_offsets if sublist is not None
                             for item in sublist])

        write_streams = len(write_offsets)
        read_streams = len(read_offsets) + write_streams  # write-allocate
        total_loads = read_streams * element_size
        # total_evicts = write_streams * element_size
        bw, measurement_kernel = self.machine.get_bandwidth(
            0,
            read_streams - write_streams,  # no write-allocate in L1
            write_streams,
            threads_per_core,
            cores=self.cores)

        # Calculate performance (arithmetic intensity * bandwidth with
        # arithmetic intensity = flops / bytes loaded )
        if total_loads == 0:
            # This happens in case of full-caching
            arith_intens = None
            performance = None
        else:
            arith_intens = float(total_flops)/total_loads
            performance = PrefixedUnit(arith_intens * float(bw), 'FLOP/s')

        self.results['mem bottlenecks'].append({
            'performance': self.conv_perf(PrefixedUnit(performance, 'FLOP/s')),
            'level': self.machine['memory hierarchy'][0]['level'],
            'arithmetic intensity': arith_intens,
            'bw kernel': measurement_kernel,
            'bandwidth': bw,
            'bytes transfered': total_loads})
        self.results['bottleneck level'] = len(self.results['mem bottlenecks'])-1
        self.results['min performance'] = self.conv_perf(performance)

        # for other cache and memory levels:
        for cache_level, cache_info in list(enumerate(self.machine['memory hierarchy']))[:-1]:
            # Compiling stats (in bytes!)
            total_misses = self.results['misses'][cache_level]*cacheline_size
            total_evicts = self.results['evicts'][cache_level]*cacheline_size

            # choose bw according to cache level and problem
            # first, compile stream counts at current cache level
            # write-allocate is allready resolved above
            read_streams = self.results['misses'][cache_level]
            write_streams = self.results['evicts'][cache_level]
            # second, try to find best fitting kernel (closest to stream seen stream counts):
            bw, measurement_kernel = self.machine.get_bandwidth(
                cache_level+1, read_streams, write_streams, threads_per_core,
                cores=self.cores)

            # Calculate performance (arithmetic intensity * bandwidth with
            # arithmetic intensity = flops / bytes transfered)
            bytes_transfered = total_misses + total_evicts

            if bytes_transfered == 0:
                # This happens in case of full-caching
                arith_intens = float('inf')
                performance = PrefixedUnit(float('inf'), 'FLOP/s')
            else:
                arith_intens = float(total_flops)/bytes_transfered
                performance = PrefixedUnit(arith_intens * float(bw), 'FLOP/s')

            self.results['mem bottlenecks'].append({
                'performance': self.conv_perf(performance),
                'level': (self.machine['memory hierarchy'][cache_level + 1]['level']),
                'arithmetic intensity': arith_intens,
                'bw kernel': measurement_kernel,
                'bandwidth': bw,
                'bytes transfered': bytes_transfered})
            if performance < self.results.get('min performance', {'FLOP/s': performance})['FLOP/s']:
                self.results['bottleneck level'] = len(self.results['mem bottlenecks'])-1
                self.results['min performance'] = self.conv_perf(performance)

        return self.results

    def analyze(self):
        """Run analysis."""
        precision = 'DP' if self.kernel.datatype == 'double' else 'SP'
        self.calculate_cache_access()

        self.results['max_perf'] = self.conv_perf(self.machine['clock'] * self.cores * \
            self.machine['FLOPs per cycle'][precision]['total'])

    def conv_perf(self, performance):
        """Convert performance (FLOP/s) to other units, such as It/s or cy/CL."""
        clock = self.machine['clock']
        flops_per_it = sum(self.kernel._flops.values())
        it_s = performance/flops_per_it
        it_s.unit = 'It/s'
        element_size = self.kernel.datatypes_size[self.kernel.datatype]
        elements_per_cacheline = int(float(self.machine['cacheline size'])) / element_size
        cy_cl = clock/it_s*elements_per_cacheline
        cy_cl.unit = 'cy/CL'
        cy_it = clock/it_s
        cy_it.unit = 'cy/It'

        return {'It/s': it_s,
                'cy/CL': cy_cl,
                'cy/It': cy_it,
                'FLOP/s': performance}

    def report(self, output_file=sys.stdout):
        """Report analysis outcome in human readable form."""
        max_perf = self.results['max_perf']

        if self._args and self._args.verbose >= 3:
            print('{}'.format(pformat(self.results)), file=output_file)

        if self._args and self._args.verbose >= 1:
            print('{}'.format(pformat(self.results['verbose infos'])), file=output_file)
            print('Bottlenecks:', file=output_file)
            print('  level | a. intensity |   performance   |   peak bandwidth  | peak bandwidth kernel',
                  file=output_file)
            print('--------+--------------+-----------------+-------------------+----------------------',
                  file=output_file)
            print('    CPU |              | {!s:>15} |                   |'.format(
                max_perf[self._args.unit]),
                  file=output_file)
            for b in self.results['mem bottlenecks']:
                print('{level:>7} | {arithmetic intensity:>5.2} FLOP/B | {0!s:>15} |'
                      ' {bandwidth!s:>17} | {bw kernel:<8}'.format(
                          b['performance'][self._args.unit], **b),
                      file=output_file)
            print('', file=output_file)

        if self.results['min performance']['FLOP/s'] > max_perf['FLOP/s']:
            # CPU bound
            print('CPU bound. {!s} due to CPU max. FLOP/s'.format(max_perf), file=output_file)
        else:
            # Cache or mem bound
            print('Cache or mem bound.', file=output_file)

            bottleneck = self.results['mem bottlenecks'][self.results['bottleneck level']]
            print('{!s} due to {} transfer bottleneck (with bw from {} benchmark)'.format(
                    bottleneck['performance'][self._args.unit],
                    bottleneck['level'],
                    bottleneck['bw kernel']),
                  file=output_file)
            print('Arithmetic Intensity: {:.2f} FLOP/B'.format(bottleneck['arithmetic intensity']),
                  file=output_file)

class RooflineIACA(Roofline):
    """
    Representation of the Roofline model based on IACA throughput analysis.

    more info to follow...
    """

    name = "Roofline (with IACA throughput)"

    @classmethod
    def configure_arggroup(cls, parser):
        """Configure argument parser."""
        pass

    def __init__(self, kernel, machine, args=None, parser=None, asm_block='auto',
                 pointer_increment='auto', cores=1, predictor=LayerConditionPredictor, verbose=0):
        """
        Create Roofline model with IACA analysis from kernel and machine objects.

        *kernel* is a Kernel object
        *machine* describes the machine (cpu, cache and memory) characteristics
        *args* (optional) are the parsed arguments from the comand line
        if *args* is given also *parser* has to be provided

        If *args* is None, *asm_block*, *pointer_increment* and *verbose* will be used, otherwise
        *args* takes precedence.
        """
        Roofline.__init__(self, kernel, machine, args, parser, cores, predictor, verbose)
        self.results = None

        if args:
            # handle CLI info
            self.asm_block = self._args.asm_block
            self.pointer_increment = self._args.pointer_increment
        else:
            self.asm_block = asm_block
            self.pointer_increment = pointer_increment

        # Validate arguments
        if self.asm_block not in ['auto', 'manual']:
            try:
                self.asm_block = int(args.asm_block)
            except ValueError:
                parser.error('asm_block can only be "auto", "manual" or an integer')
        if self.pointer_increment not in ['auto', 'auto_with_manual_fallback', 'manual']:
            try:
                self.pointer_increment = int(args.pointer_increment)
            except ValueError:
                parser.error('pointer_increment can only be "auto", '
                             '"auto_with_manual_fallback", "manual" or an integer')

    def analyze(self):
        """Run complete analysis."""
        self.results = self.calculate_cache_access()
        try:
            iaca_analysis, asm_block = self.kernel.iaca_analysis(
                micro_architecture=self.machine['micro-architecture'],
                asm_block=self.asm_block,
                pointer_increment=self.pointer_increment,
                verbose=self.verbose > 2)
        except RuntimeError as e:
            print("IACA analysis failed: " + str(e))
            sys.exit(1)

        block_throughput = iaca_analysis['throughput']
        uops = iaca_analysis['uops']
        iaca_output = iaca_analysis['output']
        port_cycles = iaca_analysis['port cycles']

        # Normalize to cycles per cacheline
        elements_per_block = abs(asm_block['pointer_increment']
                                 / self.kernel.datatypes_size[self.kernel.datatype])
        block_size = elements_per_block*self.kernel.datatypes_size[self.kernel.datatype]
        try:
            block_to_cl_ratio = float(self.machine['cacheline size'])/block_size
        except ZeroDivisionError as e:
            print("Too small block_size / pointer_increment:", e, file=sys.stderr)
            sys.exit(1)

        port_cycles = dict([(i[0], i[1]*block_to_cl_ratio) for i in list(port_cycles.items())])
        uops = uops*block_to_cl_ratio
        cl_throughput = block_throughput*block_to_cl_ratio
        flops_per_element = sum(self.kernel._flops.values())

        # Overwrite CPU-L1 stats, because they are covered by IACA
        self.results['mem bottlenecks'][0] = None

        # Reevaluate mem bottleneck
        self.results['min performance'] = self.conv_perf(PrefixedUnit(float('inf'), 'FLOP/s'))
        self.results['bottleneck level'] = None
        for level, bottleneck in enumerate(self.results['mem bottlenecks']):
            if level == 0:
                # ignoring CPU-L1
                continue
            if bottleneck['performance']['FLOP/s'] < self.results['min performance']['FLOP/s']:
                self.results['bottleneck level'] = level
                self.results['min performance'] = bottleneck['performance']

        # Create result dictionary
        self.results.update({
            'cpu bottleneck': {
                'port cycles': port_cycles,
                'cl throughput': cl_throughput,
                'uops': uops,
                'performance throughput': self.conv_perf(PrefixedUnit(
                    self.machine['clock']/block_throughput*elements_per_block*flops_per_element
                    * self.cores, "FLOP/s")),
                'IACA output': iaca_output}})

    def report(self, output_file=sys.stdout):
        """Print human readable report of model."""
        cpu_perf = self.results['cpu bottleneck']['performance throughput']

        if self.verbose >= 3:
            print('{}'.format(pformat(self.results)), file=output_file)

        if self.verbose >= 1:
            print('Bottlenecks:', file=output_file)
            print('  level | a. intensity |   performance   |   peak bandwidth  | peak bandwidth kernel',
                  file=output_file)
            print('--------+--------------+-----------------+-------------------+----------------------',
                  file=output_file)
            print('    CPU |              | {!s:>15} |                   |'.format(
                cpu_perf[self._args.unit]),
                  file=output_file)
            for b in self.results['mem bottlenecks']:
                # Skip CPU-L1 from Roofline model
                if b is None:
                    continue
                print('{level:>7} | {arithmetic intensity:>5.2} FLOP/B | {0!s:>15} |'
                      ' {bandwidth!s:>17} | {bw kernel:<8}'.format(
                          b['performance'][self._args.unit], **b),
                      file=output_file)
            print('', file=output_file)
            print('IACA analisys:', file=output_file)
            print('{!s}'.format(
                {k: v
                 for k, v in list(self.results['cpu bottleneck'].items())
                 if k not in['IACA output']}),
                file=output_file)

        if self.results['min performance']['FLOP/s'] > cpu_perf['FLOP/s']:
            # CPU bound
            print('CPU bound. {!s} due to CPU bottleneck'.format(cpu_perf[self._args.unit]),
                  file=output_file)
        else:
            # Cache or mem bound
            print('Cache or mem bound.', file=output_file)

            bottleneck = self.results['mem bottlenecks'][self.results['bottleneck level']]
            print('{!s} due to {} transfer bottleneck (with bw from {} benchmark)'.format(
                      bottleneck['performance'][self._args.unit],
                      bottleneck['level'],
                      bottleneck['bw kernel']),
                  file=output_file)
            print('Arithmetic Intensity: {:.2f} FLOP/B'.format(bottleneck['arithmetic intensity']),
                  file=output_file)


#!/usr/bin/env python3
"""Layer condition model and helper functions"""
import sys
from itertools import chain
from pprint import pprint
from collections import defaultdict
from functools import cmp_to_key

import sympy

from .base import PerformanceModel

class LC(PerformanceModel):
    """
    Representation of the layer condition model.

    See https://rrze-hpc.github.io/layer condition/ for information about this analytical model.
    """

    name = "Layer Condition"

    @classmethod
    def configure_arggroup(cls, parser):
        """Configure arugment parser"""
        pass

    def __init__(self, kernel, machine, args=None, parser=None):
        """
        Create layer condition model from kernel and machine objects.

        *kernel* is a Kernel object
        *machine* describes the machine (cpu, cache and memory) characteristics
        *args* (optional) are the parsed arguments from the comand line
        """
        self.kernel = kernel
        self.machine = machine
        self._args = args
        self._parser = parser
        self.results = None

        if args:
            # handle CLI info
            pass

    def calculate_cache_access(self):
        """Apply layer condition model to calculate cache accesses."""
        # FIXME handle multiple datatypes
        element_size = self.kernel.datatypes_size[self.kernel.datatype]

        results = {'dimensions': {}}

        def sympy_compare(a, b):
            c = 0
            for i in range(min(len(a), len(b))):
                s = a[i] - b[i]
                if sympy.simplify(s > 0):
                    c = -1
                elif sympy.simplify(s == 0):
                    c = 0
                else:
                    c = 1
                if c != 0:
                    break
            return c

        accesses = defaultdict(list)
        sympy_accesses = defaultdict(list)
        for var_name in self.kernel.variables:
            for r in self.kernel.sources.get(var_name, []):
                if r is None:
                    continue
                accesses[var_name].append(r)
                sympy_accesses[var_name].append(self.kernel.access_to_sympy(var_name, r))
            for w in self.kernel.destinations.get(var_name, []):
                if w is None:
                    continue
                accesses[var_name].append(w)
                sympy_accesses[var_name].append(self.kernel.access_to_sympy(var_name, w))
            # order accesses by increasing order
            accesses[var_name].sort(key=cmp_to_key(sympy_compare), reverse=True)

        results['accesses'] = accesses
        results['sympy_accesses'] = sympy_accesses

        # For each dimension (1D, 2D, 3D ... nD)
        for dimension in range(1, len(list(self.kernel.get_loop_stack()))+1):
            results['dimensions'][dimension] = {}

            slices = defaultdict(list)
            slices_accesses = defaultdict(list)
            for var_name in accesses:
                for a in accesses[var_name]:
                    # slices are identified by the tuple of indices of higher dimensions
                    slice_id = tuple([var_name, tuple(a[:-dimension])])
                    slices[slice_id].append(a)
                    slices_accesses[slice_id].append(self.kernel.access_to_sympy(var_name, a))
            results['dimensions'][dimension]['slices'] = slices
            results['dimensions'][dimension]['slices_accesses'] = slices_accesses

            slices_distances = defaultdict(list)
            for k, v in slices_accesses.items():
                for i in range(1, len(v)):
                    slices_distances[k].append((v[i] - v[i-1]).simplify())
            results['dimensions'][dimension]['slices_distances'] = slices_distances

            # Check that distances contain only free_symbols based on constants
            for dist in chain(*slices_distances.values()):
                if any([s not in self.kernel.constants.keys() for s in dist.free_symbols]):
                    raise ValueError("Some distances are not based on constants: "+str(dist))

            # Sum of lengths between relative distances
            slices_sum = sum([sum(dists) for dists in slices_distances.values()])
            results['dimensions'][dimension]['slices_sum'] = slices_sum

            # Max of lengths between relative distances
            # Work-around, the arguments with the most symbols get to stay
            # FIXME, may not be correct in all cases. e.g., N+M vs. N*M
            def FuckedUpMax(*args):
                if len(args) == 1:
                    return args[0]
                # expand all expressions:
                args = [a.expand() for a in args]
                # Filter expressions with less than the maximum number of symbols
                max_symbols = max([len(a.free_symbols) for a in args])
                args = list(filter(lambda a: len(a.free_symbols) == max_symbols, args))
                if max_symbols == 0:
                    return sympy.Max(*args)
                # Filter symbols with lower exponent
                max_coeffs = 0
                for a in args:
                    for s in a.free_symbols:
                        max_coeffs = max(max_coeffs, len(sympy.Poly(a, s).all_coeffs()))

                def coeff_filter(a):
                    return max(
                        0, 0,
                        *[len(sympy.Poly(a, s).all_coeffs()) for s in a.free_symbols]) == max_coeffs
                args = list(filter(coeff_filter, args))

                m = sympy.Max(*args)
                # if m.is_Function:
                #     raise ValueError("Could not resolve {} to maximum.".format(m))
                return m

            slices_max = FuckedUpMax(sympy.Integer(0),
                                     *[FuckedUpMax(*dists) for dists in slices_distances.values()])
            results['dimensions'][dimension]['slices_max'] = slices_max

            # Nmber of slices
            slices_count = len(slices_accesses)
            results['dimensions'][dimension]['slices_count'] = slices_count

            # Cache requirement expression
            cache_requirement_bytes = (slices_sum + slices_max*slices_count)*element_size
            results['dimensions'][dimension]['cache_requirement_bytes'] = cache_requirement_bytes
            # Apply to all cache sizes
            csim = self.machine.get_cachesim(self._args.cores)
            results['dimensions'][dimension]['caches'] = {}
            for cl in csim.levels(with_mem=False):
                cache_equation = sympy.Eq(cache_requirement_bytes, cl.size())
                if len(self.kernel.constants.keys()) <= 1:
                    inequality = sympy.solve(sympy.LessThan(cache_requirement_bytes, cl.size()),
                                             *self.kernel.constants.keys())
                else:
                    # Sympy does not solve for multiple constants
                    inequality = sympy.LessThan(cache_requirement_bytes, cl.size())
                try:
                    eq = sympy.solve(inequality, *self.kernel.constants.keys(), dict=True)
                except NotImplementedError:
                    eq = None
                results['dimensions'][dimension]['caches'][cl.name] = {
                    'cache_size': cl.size(),
                    'equation': cache_equation,
                    'lt': inequality,
                    'eq': eq
                }

        return results

    def analyze(self):
        """Run complete analysis."""
        # check that layer conditions can be applied on this kernel:
        # 1. All iterations may only have a step width of 1
        loop_stack = list(self.kernel.get_loop_stack())
        if any([l['increment'] != 1 for l in loop_stack]):
            raise ValueError("Can not apply layer condition, since not all loops are of step "
                             "length 1.")

        # 2. The order of iterations must be reflected in the order of indices in all array
        #    references containing the inner loop index. If the inner loop index is not part of the
        #    reference, the reference is simply ignored
        # TODO support flattend array indexes
        for aref in list(self.kernel.index_order()):
            # Strip left most empty sets (refereces without index)
            while aref and len(aref[0]) == 0:
                aref.pop(0)
            for i, idx_names in enumerate(aref):
                # 1. Check for that there are enough loops to handle access dimensions
                # 2. Check that offset index matches loop index (in same order)
                if i >= len(loop_stack) or \
                        any([loop_stack[i]['index'] != idx.name for idx in idx_names]):
                    raise ValueError("Can not apply layer condition, order of indices in array "
                                     "does not follow order of loop indices. Single-dimension is "
                                     "currently not supported.")

        # 3. Indices may only increase with one
        # TODO use a public interface, not self.kernel._*
        for arefs in chain(chain(*self.kernel.sources.values()),
                           chain(*self.kernel.destinations.values())):
            if not arefs:
                continue

            # Strip left most constant offsets (refereces without index) to support things like:
            # a[0][i+1][j][k-1] with an i, j and k loop-nest
            while arefs and not arefs[0].free_symbols:
                arefs = arefs[1:]

            # Check that remaining indices are in orde
            for i, expr in enumerate(arefs):
                diff = sympy.diff(expr, sympy.Symbol(loop_stack[i]['index']))
                if diff != 0 and diff != 1:
                    # TODO support -1 aswell
                    raise ValueError("Can not apply layer condition, array references may not "
                                     "increment more then one per iteration.")

        self.results = self.calculate_cache_access()

    def report(self, output_file=sys.stdout):
        """Report generated model in human readable form."""
        if self._args and self._args.verbose > 2:
            pprint(self.results)

        for dimension, lc_info in self.results['dimensions'].items():
            print("{}D layer condition:".format(dimension), file=output_file)
            for cache, lc_solution in sorted(lc_info['caches'].items()):
                print(cache+": ", end='', file=output_file)
                if lc_solution['lt'] is sympy.true:
                    print("unconditionally fulfilled", file=output_file)
                else:
                    if lc_solution['eq'] is None:
                        print("{}".format(lc_solution['lt']), file=output_file)
                    elif type(lc_solution['eq']) is not list:
                        print("{}".format(lc_solution['eq']), file=output_file)
                    else:
                        for solu in lc_solution['eq']:
                            for s, v in solu.items():
                                print("{} <= {}".format(s, v), file=output_file)


#!/usr/bin/env python3
"""Collection of functions that extend pycparser's functionality."""

import collections

from pycparser import c_ast

def clean_code(code, comments=True, macros=False, pragmas=False):
    """
    Naive comment and macro striping from source code

    :param comments: If True, all comments are stripped from code
    :param macros: If True, all macros are stripped from code
    :param pragmas: If True, all pragmas are stripped from code

    :return: cleaned code. Line numbers are preserved with blank lines,
    and multiline comments and macros are supported. BUT comment-like
    strings are (wrongfully) treated as comments.
    """
    if macros or pragmas:
        lines = code.split('\n')
        in_macro = False
        in_pragma = False
        for i in range(len(lines)):
            l = lines[i].strip()

            if macros and (l.startswith('#') and not l.startswith('#pragma') or in_macro):
                lines[i] = ''
                in_macro = l.endswith('\\')
            if pragmas and (l.startswith('#pragma') or in_pragma):
                lines[i] = ''
                in_pragma = l.endswith('\\')
        code = '\n'.join(lines)

    if comments:
        idx = 0
        comment_start = None
        while idx < len(code) - 1:
            if comment_start is None and code[idx:idx + 2] == '//':
                end_idx = code.find('\n', idx)
                code = code[:idx] + code[end_idx:]
                idx -= end_idx - idx
            elif comment_start is None and code[idx:idx + 2] == '/*':
                comment_start = idx
            elif comment_start is not None and code[idx:idx + 2] == '*/':
                code = (code[:comment_start] +
                        '\n' * code[comment_start:idx].count('\n') +
                        code[idx + 2:])
                idx -= idx - comment_start
                comment_start = None
            idx += 1

    return code

def replace_id(ast, id_name, replacement):
    """
    Replace all matching ID nodes in ast (in-place), with replacement.

    :param id_name: name of ID node to match
    :param replacement: single or list of node to insert in replacement for ID node.
    """
    for a in ast:
        if isinstance(a, c_ast.ID) and a.name == id_name:
            # Check all attributes of ast
            for attr_name in dir(ast):
                # Exclude special and method attributes
                if attr_name.startswith('__') or callable(getattr(ast, attr_name)):
                    continue
                attr = getattr(ast, attr_name)
                # In case of direct match, just replace
                if attr is a:
                    setattr(ast, attr_name, replacement)
                # If contained in list replace occurrence with replacement
                if type(attr) is list:
                    for i, attr_element in enumerate(attr):
                        if attr_element is a:
                            if type(replacement) is list:
                                # If replacement is list, inject
                                attr[i:i+1] = replacement
                            else:
                                # otherwise replace
                                attr[i] = replacement
        else:
            replace_id(a, id_name, replacement)


#!/usr/bin/env python3
"""Execution-Cache-Memory model class and helper functions."""
import copy
import sys
import math
import pprint

import sympy
try:
    import matplotlib
    matplotlib.use('Agg')
    import matplotlib.pyplot as plt
    plot_support = True
except ImportError:
    plot_support = False

from kerncraft.prefixedunit import PrefixedUnit
from kerncraft.cacheprediction import LayerConditionPredictor, CacheSimulationPredictor
from .base import PerformanceModel

def round_to_next(x, base):
    """Round float to next multiple of base."""
    # Based on: http://stackoverflow.com/a/2272174
    return int(base * math.ceil(float(x)/base))

def blocking(indices, block_size, initial_boundary=0):
    """
    Split list of integers into blocks of block_size and return block indices.

    First block element will be located at initial_boundary (default 0).

    >>> blocking([0, -1, -2, -3, -4, -5, -6, -7, -8, -9], 8)
    [0,-1]
    >>> blocking([0], 8)
    [0]
    >>> blocking([0], 8, initial_boundary=32)
    [-4]
    """
    blocks = []

    for idx in indices:
        bl_idx = (idx-initial_boundary)//float(block_size)
        if bl_idx not in blocks:
            blocks.append(bl_idx)
    blocks.sort()

    return blocks

class ECMData(PerformanceModel):
    """Representation of Data portion of the Execution-Cache-Memory Model."""

    name = "Execution-Cache-Memory (data transfers only)"

    def __init__(self, kernel, machine, args=None, parser=None, cores=1,
                 cache_predictor=CacheSimulationPredictor, verbose=0):
        """
        Create Execcution-Cache-Memory data model from kernel and machine objects.

        *kernel* is a Kernel object
        *machine* describes the machine (cpu, cache and memory) characteristics
        *args* (optional) are the parsed arguments from the comand line

        If *args* is None, *cores*, *cache_predictor* and *verbose* are taken into account,
        otherwise *args* takes precedence.
        """
        self.kernel = kernel
        self.machine = machine
        self._args = args
        self._parser = parser
        self.results = {}

        if args:
            self.verbose = self._args.verbose
            self.cores = self._args.cores
            if self._args.cache_predictor == 'SIM':
                self.predictor = CacheSimulationPredictor(self.kernel, self.machine, self.cores)
            elif self._args.cache_predictor == 'LC':
                self.predictor = LayerConditionPredictor(self.kernel, self.machine, self.cores)
            else:
                raise NotImplementedError("Unknown cache predictor, only LC (layer condition) and "
                                          "SIM (cache simulation with pycachesim) is supported.")
        else:
            self.cores = cores
            self.predictor = cache_predictor(self.kernel, self.machine, self.cores)
            self.verbose = verbose

    def calculate_cache_access(self):
        """Dispatch to cache predictor to get cache stats."""
        self.results.update({
                        'cycles': [],  # will be filled by caclculate_cycles()
                        'misses': self.predictor.get_misses(),
                        'hits': self.predictor.get_hits(),
                        'evicts': self.predictor.get_evicts(),
                        'verbose infos': self.predictor.get_infos()})  # only for verbose outputs

    def calculate_cycles(self):
        """
        Calculate performance model cycles from cache stats.

        calculate_cache_access() needs to have been execute before.
        """
        element_size = self.kernel.datatypes_size[self.kernel.datatype]
        elements_per_cacheline = float(self.machine['cacheline size']) // element_size
        iterations_per_cacheline = (sympy.Integer(self.machine['cacheline size']) /
                                    sympy.Integer(self.kernel.bytes_per_iteration))
        self.results['iterations per cacheline'] = iterations_per_cacheline
        cacheline_size = float(self.machine['cacheline size'])

        loads, stores = (self.predictor.get_loads(), self.predictor.get_stores())

        for cache_level, cache_info in list(enumerate(self.machine['memory hierarchy']))[1:]:
            throughput, duplexness = cache_info['non-overlap upstream throughput']

            if type(throughput) is str and throughput == 'full socket memory bandwidth':
                # Memory transfer
                # we use bandwidth to calculate cycles and then add panalty cycles (if given)

                # choose bw according to cache level and problem
                # first, compile stream counts at current cache level
                # write-allocate is allready resolved in cache predictor
                read_streams = loads[cache_level]
                write_streams = stores[cache_level]
                # second, try to find best fitting kernel (closest to stream seen stream counts):
                threads_per_core = 1
                bw, measurement_kernel = self.machine.get_bandwidth(
                    cache_level, read_streams, write_streams, threads_per_core)

                # calculate cycles
                if duplexness == 'half-duplex':
                    cycles = float(loads[cache_level] + stores[cache_level]) * \
                             float(elements_per_cacheline) * float(element_size) * \
                             float(self.machine['clock']) / float(bw)
                else:  # full-duplex
                    raise NotImplementedError(
                        "full-duplex mode is not (yet) supported for memory transfers.")
                # add penalty cycles for each read stream
                if 'penalty cycles per read stream' in cache_info:
                    cycles += stores[cache_level] * \
                              cache_info['penalty cycles per read stream']

                self.results.update({
                    'memory bandwidth kernel': measurement_kernel,
                    'memory bandwidth': bw})
            else:
                # since throughput is given in B/cy, and we need CL/cy:
                throughput = float(throughput) / cacheline_size
                # only cache cycles count
                if duplexness == 'half-duplex':
                    cycles = (loads[cache_level] + stores[cache_level]) / float(throughput)
                elif duplexness == 'full-duplex':
                    cycles = max(loads[cache_level] / float(throughput),
                                 stores[cache_level] / float(throughput))
                else:
                    raise ValueError("Duplexness of cache throughput may only be 'half-duplex'"
                                     "or 'full-duplex', found {} in {}.".format(
                        duplexness, cache_info['name']))

            self.results['cycles'].append((cache_info['level'], cycles))

            self.results[cache_info['level']] = cycles

        return self.results

    def analyze(self):
        """Run complete anaylysis and return results."""
        self.calculate_cache_access()
        self.calculate_cycles()
        self.results['flops per iteration'] = sum(self.kernel._flops.values())

        return self.results

    def conv_cy(self, cy_cl):
        """Convert cycles (cy/CL) to other units, such as FLOP/s or It/s."""
        if not isinstance(cy_cl, PrefixedUnit):
            cy_cl = PrefixedUnit(cy_cl, '', 'cy/CL')

        clock = self.machine['clock']
        element_size = self.kernel.datatypes_size[self.kernel.datatype]
        elements_per_cacheline = int(self.machine['cacheline size']) // element_size
        if cy_cl != 0:
            it_s = clock/cy_cl*elements_per_cacheline
            it_s.unit = 'It/s'
        else:
            it_s = PrefixedUnit('inf It/S')
        flops_per_it = sum(self.kernel._flops.values())
        performance = it_s*flops_per_it
        performance.unit = 'FLOP/s'
        cy_it = cy_cl*elements_per_cacheline
        cy_it.unit = 'cy/It'

        return {'It/s': it_s,
                'cy/CL': cy_cl,
                'cy/It': cy_it,
                'FLOP/s': performance}

    def report_data_transfers(self):
        cacheline_size = float(self.machine['cacheline size'])
        r = "Data Transfers:\nLevel   | Loads    | Store    |\n"
        loads, stores = (self.predictor.get_loads(), self.predictor.get_stores())
        for cache_level, cache_info in list(enumerate(self.machine['memory hierarchy']))[1:]:
            r += ("{:>7} | {:>3.0f} B/CL | {:>3.0f} B/CL |\n".format(
                self.machine['memory hierarchy'][cache_level-1]['level']+'-'+cache_info['level'],
                loads[cache_level] * cacheline_size,
                stores[cache_level] * cacheline_size))
        return r

    def report(self, output_file=sys.stdout):
        """Print generated model data in human readable format."""
        if self.verbose > 1:
            print('{}'.format(pprint.pformat(self.results['verbose infos'])), file=output_file)

        for level, cycles in self.results['cycles']:
            print('{} = {}'.format(
                level, self.conv_cy(cycles)[self._args.unit]), file=output_file)

        if self.verbose > 1:
            if 'memory bandwidth kernel' in self.results:
                print('memory cycles based on {} kernel with {}'.format(
                          self.results['memory bandwidth kernel'],
                          self.results['memory bandwidth']),
                      file=output_file)

        if self.verbose > 1:
            print(file=output_file)
            print(self.report_data_transfers(), file=output_file)

class ECMCPU(PerformanceModel):
    """Representation of the In-core execution part of the Execution-Cache-Memory model."""

    name = "Execution-Cache-Memory (CPU operations only)"

    @classmethod
    def configure_arggroup(cls, parser):
        """Configure argument parser."""
        pass

    def __init__(self, kernel, machine, args=None, parser=None, asm_block='auto',
                 pointer_increment='auto', verbose=0):
        """
        Create Execution-Cache-Memory model from kernel and machine objects.

        *kernel* is a Kernel object
        *machine* describes the machine (cpu, cache and memory) characteristics
        *args* (optional) are the parsed arguments from the comand line
        if *args* is given also *parser* has to be provided

        If *args* is None, *asm_block*, *pointer_increment* and *verbose* will be used, otherwise
        *args* takes precedence.
        """
        self.kernel = kernel
        self.machine = machine
        self._args = args
        self._parser = parser
        self.results = {}

        if args:
            # handle CLI info
            self.asm_block = self._args.asm_block
            self.pointer_increment = self._args.pointer_increment
            self.verbose = self._args.verbose
        else:
            self.asm_block = asm_block
            self.pointer_increment = pointer_increment
            self.verbose = verbose

        # Validate arguments
        if self.asm_block not in ['auto', 'manual']:
            try:
                self.asm_block = int(args.asm_block)
            except ValueError:
                parser.error('asm_block can only be "auto", "manual" or an integer')
        if self.pointer_increment not in ['auto', 'auto_with_manual_fallback', 'manual']:
            try:
                self.pointer_increment = int(args.pointer_increment)
            except ValueError:
                parser.error('pointer_increment can only be "auto", '
                             '"auto_with_manual_fallback", "manual" or an integer')

    def analyze(self):
        """
        Run complete analysis and return results.
        """
        try:
            incore_analysis, asm_block =  self.kernel.iaca_analysis(
                micro_architecture=self.machine['micro-architecture'],
                asm_block=self.asm_block,
                pointer_increment=self.pointer_increment,
                verbose=self.verbose > 2)
        except RuntimeError as e:
            print("IACA analysis failed: " + str(e))
            sys.exit(1)

        block_throughput = incore_analysis['throughput']
        port_cycles = incore_analysis['port cycles']
        uops = incore_analysis['uops']

        # Normalize to cycles per cacheline
        elements_per_block = abs(asm_block['pointer_increment']
                                 // self.kernel.datatypes_size[self.kernel.datatype])
        block_size = elements_per_block*self.kernel.datatypes_size[self.kernel.datatype]
        try:
            block_to_cl_ratio = float(self.machine['cacheline size'])/block_size
        except ZeroDivisionError as e:
            print("Too small block_size / pointer_increment:", e, file=sys.stderr)
            sys.exit(1)

        port_cycles = dict([(i[0], i[1]*block_to_cl_ratio) for i in list(port_cycles.items())])
        uops = uops*block_to_cl_ratio
        cl_throughput = block_throughput*block_to_cl_ratio

        # Compile most relevant information
        T_OL = max([v for k, v in list(port_cycles.items())
                    if k in self.machine['overlapping model']['ports']])
        T_nOL = max([v for k, v in list(port_cycles.items())
                     if k in self.machine['non-overlapping model']['ports']])

        # Use IACA throughput prediction if it is slower then T_nOL
        if T_nOL < cl_throughput:
            T_OL = cl_throughput

        # Create result dictionary
        self.results = {
            'port cycles': port_cycles,
            'cl throughput': self.conv_cy(cl_throughput),
            'uops': uops,
            'T_nOL': T_nOL,
            'T_OL': T_OL,
            'IACA output': incore_analysis['output'],
            'elements_per_block': elements_per_block,
            'pointer_increment': asm_block['pointer_increment'],
            'flops per iteration': sum(self.kernel._flops.values())}
        return self.results

    def conv_cy(self, cy_cl):
        """Convert cycles (cy/CL) to other units, such as FLOP/s or It/s."""
        if not isinstance(cy_cl, PrefixedUnit):
            cy_cl = PrefixedUnit(cy_cl, '', 'cy/CL')
        clock = self.machine['clock']
        element_size = self.kernel.datatypes_size[self.kernel.datatype]
        elements_per_cacheline = int(self.machine['cacheline size']) // element_size
        it_s = clock/cy_cl*elements_per_cacheline
        it_s.unit = 'It/s'
        flops_per_it = sum(self.kernel._flops.values())
        performance = it_s*flops_per_it
        performance.unit = 'FLOP/s'
        cy_it = cy_cl*elements_per_cacheline
        cy_it.unit = 'cy/It'

        return {'It/s': it_s,
                'cy/CL': cy_cl,
                'cy/It': cy_it,
                'FLOP/s': performance}

    def report(self, output_file=sys.stdout):
        """Print generated model data in human readable format."""
        if self.verbose > 2:
            print("IACA Output:", file=output_file)
            print(self.results['IACA output'], file=output_file)
            print('', file=output_file)

        if self.verbose > 1:
            print('Detected pointer increment: {}'.format(self.results['pointer_increment']),
                  file=output_file)
            print('Derived elements stored to per asm block iteration: {}'.format(
                  self.results['elements_per_block']), file=output_file)
            print('Ports and cycles:', str(self.results['port cycles']), file=output_file)
            print('Uops:', str(self.results['uops']), file=output_file)

            print('Throughput: {}'.format(self.results['cl throughput'][self._args.unit]),
                  file=output_file)

        print('T_nOL = {:.1f} cy/CL'.format(self.results['T_nOL']), file=output_file)
        print('T_OL = {:.1f} cy/CL'.format(self.results['T_OL']), file=output_file)

class ECM(PerformanceModel):
    """
    Complete representation of the Execution-Cache-Memory Model (data and operations).

    more info to follow...
    """

    name = "Execution-Cache-Memory"

    @classmethod
    def configure_arggroup(cls, parser):
        """Configure argument parser."""
        # others are being configured in ECMData and ECMCPU
        parser.add_argument(
            '--ecm-plot',
            help='Filename to save ECM plot to (supported extensions: pdf, png, svg and eps)')

    def __init__(self, kernel, machine, args=None, parser=None, asm_block="auto",
                 pointer_increment="auto", cores=1, cache_predictor=CacheSimulationPredictor,
                 verbose=0):
        """
        Create complete Execution-Cache-Memory model from kernel and machine objects.

        *kernel* is a Kernel object
        *machine* describes the machine (cpu, cache and memory) characteristics
        *args* (optional) are the parsed arguments from the comand line

        If *args* is None, *asm_block*, *pointer_increment*, *cores*, *cache_predictor* and
        *verbose* will be used, otherwise *args* takes precedence.
        """
        self.kernel = kernel
        self.machine = machine
        self._args = args
        self.verbose = verbose
        self.results = None

        if args:
            self.verbose = self._args.verbose

        self._CPU = ECMCPU(kernel, machine, args, parser, asm_block=asm_block,
                           pointer_increment=pointer_increment, verbose=verbose)
        self._data = ECMData(kernel, machine, args, parser,
                             cache_predictor=CacheSimulationPredictor, cores=cores, verbose=verbose)

    def analyze(self):
        """Run complete analysis."""
        self._CPU.analyze()
        self._data.analyze()
        self.results = copy.deepcopy(self._CPU.results)
        self.results.update(copy.deepcopy(self._data.results))

        cores_per_numa_domain = self.machine['cores per NUMA domain']

        # Compile total single-core prediction
        self.results['total cycles'] = self._CPU.conv_cy(max(
            self.results['T_OL'],
            sum([self.results['T_nOL']] + [i[1] for i in self.results['cycles']])))
        T_ECM = float(self.results['total cycles']['cy/CL'])

        # T_MEM is the cycles accounted to memory transfers
        T_MEM = self.results['cycles'][-1][1]

        # Simple scaling prediction:
        # Assumptions are:
        #  - bottleneck is always LLC-MEM
        #  - all caches scale with number of cores (bw AND size(WRONG!))

        # Full caching in higher cache level
        self.results['scaling cores'] = float('inf')
        # Not full caching:
        if self.results['cycles'][-1][1] != 0.0:
            # Considering memory bus utilization
            utilization = [0]
            self.results['scaling cores'] = float('inf')
            for c in range(1, cores_per_numa_domain + 1):
                if c * T_MEM > (T_ECM + utilization[c - 1] * (c - 1) * T_MEM / 2):
                    utilization.append(1.0)
                    self.results['scaling cores'] = min(self.results['scaling cores'], c)
                else:
                    utilization.append(c * T_MEM / (T_ECM + utilization[c - 1] * (c - 1) * T_MEM / 2))
            utilization = utilization[1:]

            # Old scaling code
            #self.results['scaling cores'] = (
            #    max(self.results['T_OL'],
            #        self.results['T_nOL'] + sum([c[1] for c in self.results['cycles']])) /
            #    self.results['cycles'][-1][1])
            scaling_predictions = []
            for cores in range(1, self.machine['cores per socket'] + 1):
                scaling = {'cores': cores, 'notes': [], 'performance': None,
                           'in-NUMA performance': None}
                # Detailed scaling:
                if cores <= self.results['scaling cores']:
                    # Is it purely in-cache?
                    innuma_rectp = PrefixedUnit(
                            max(sum([c[1] for c in self.results['cycles']]) + self.results['T_nOL'],
                                self.results['T_OL']) / (T_ECM/T_MEM),
                            "cy/CL")
                    scaling['notes'].append("memory-interface not saturated")
                else:
                    innuma_rectp = PrefixedUnit(self.results['cycles'][-1][1], 'cy/CL')
                    scaling['notes'].append("memory-interface saturated on first NUMA domain")
                # Include NUMA-local performance in results dict
                scaling['in-NUMA performance'] = innuma_rectp

                if 0 < cores <= cores_per_numa_domain:
                    # only in-numa scaling to consider
                    scaling['performance'] = self._CPU.conv_cy(
                        innuma_rectp / utilization[cores - 1])
                    scaling['notes'].append("in-NUMA-domain scaling")
                elif cores <= self.machine['cores per socket'] * self.machine['sockets']:
                    # out-of-numa scaling behavior
                    scaling['performance'] = self._CPU.conv_cy(
                        innuma_rectp * cores_per_numa_domain / cores)
                    scaling['notes'].append("out-of-NUMA-domain scaling")
                else:
                    raise ValueError("Number of cores must be greater than zero and upto the max. "
                                     "number of cores defined by cores per socket and sockets in"
                                     "machine file.")
                scaling_predictions.append(scaling)
        else:
            # pure in-cache performace (perfect scaling)
            scaling_predictions = [
                {'cores': cores, 'notes': ['pure in-cache'],
                 'performance': self._CPU.conv_cy(T_ECM/cores),
                 'in-NUMA performance': self._CPU.conv_cy(T_ECM/cores_per_numa_domain)}
                for cores in range(1, self.machine['cores per socket'] + 1)]

        # Also include prediction for all in-NUMA core counts in results
        self.results['scaling prediction'] = scaling_predictions
        if self._args.cores:
            self.results['multi-core'] = scaling_predictions[self._args.cores - 1]
        else:
            self.results['multi-core'] = None

    def report(self, output_file=sys.stdout):
        """Print generated model data in human readable format."""
        report = ''
        if self.verbose > 1:
            self._CPU.report()
            self._data.report()

        report += '{{ {:.1f} || {:.1f} | {} }} cy/CL'.format(
            self.results['T_OL'],
            self.results['T_nOL'],
            ' | '.join(['{:.1f}'.format(i[1]) for i in self.results['cycles']]))

        if self._args.cores > 1:
            report += " (single core)"

        report += ' = {}'.format(self.results['total cycles'][self._args.unit])

        report += '\n{{ {:.1f} \ {} }} cy/CL'.format(
            max(self.results['T_OL'], self.results['T_nOL']),
            ' \ '.join(['{:.1f}'.format(max(sum([x[1] for x in self.results['cycles'][:i+1]]) +
                                            self.results['T_nOL'], self.results['T_OL']))
                        for i in range(len(self.results['cycles']))]))

        if self._args.cores > 1:
            report += " (single core)"

        report += '\nsaturating at {:.0f} cores'.format(self.results['scaling cores'])

        if self.results['multi-core']:
            report += "\nprediction for {} cores,".format(self.results['multi-core']['cores']) + \
                      " assuming static scheduling: "
            report += "{} ({})\n".format(
                self.results['multi-core']['performance'][self._args.unit],
                ', '.join(self.results['multi-core']['notes']))

        if self.results['scaling prediction']:
            report += "\nScaling prediction, considering memory bus utilization penalty and " \
                "assuming all scalable caches:\n"
            if self.machine['cores per socket'] > self.machine['cores per NUMA domain']:
                report += "1st NUMA dom." + (len(self._args.unit) - 4) * ' ' + '||' + \
                    '--------' * (self.machine['cores per NUMA domain']-1) + '-------|\n'

            report +=  "cores " + (len(self._args.unit)+2)*' ' + " || " + ' | '.join(
                ['{:<5}'.format(s['cores']) for s in self.results['scaling prediction']]) + '\n'
            report +=  "perf. ({}) || ".format(self._args.unit) + ' | '.join(
                ['{:<5.1f}'.format(float(s['performance'][self._args.unit]))
                 for s in self.results['scaling prediction']]) + '\n'

        print(report, file=output_file)

        if self._args and self._args.ecm_plot:
            assert plot_support, "matplotlib couldn't be imported. Plotting is not supported."
            fig = plt.figure(frameon=False)
            self.plot(fig)

    def plot(self, fig=None):
        """Plot visualization of model prediction."""
        if not fig:
            fig = plt.gcf()

        fig.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.15)
        ax = fig.add_subplot(1, 1, 1)

        sorted_overlapping_ports = sorted(
            [(p, self.results['port cycles'][p]) for p in self.machine['overlapping ports']],
            key=lambda x: x[1])

        yticks_labels = []
        yticks = []
        xticks_labels = []
        xticks = []

        # Plot configuration
        height = 0.9

        i = 0
        # T_OL
        colors = ([(254. / 255, 177. / 255., 178. / 255.)] +
                  [(255. / 255., 255. / 255., 255. / 255.)] * (len(sorted_overlapping_ports) - 1))
        for p, c in sorted_overlapping_ports:
            ax.barh(i, c, height, align='center', color=colors.pop(),
                    edgecolor=(0.5, 0.5, 0.5), linestyle='dashed')
            if i == len(sorted_overlapping_ports) - 1:
                ax.text(c / 2.0, i, '$T_\mathrm{OL}$', ha='center', va='center')
            yticks_labels.append(p)
            yticks.append(i)
            i += 1
        xticks.append(sorted_overlapping_ports[-1][1])
        xticks_labels.append('{:.1f}'.format(sorted_overlapping_ports[-1][1]))

        # T_nOL + memory transfers
        y = 0
        colors = [(187. / 255., 255 / 255., 188. / 255.)] * (len(self.results['cycles'])) + \
                 [(119. / 255, 194. / 255., 255. / 255.)]
        for k, v in [('nOL', self.results['T_nOL'])] + self.results['cycles']:
            ax.barh(i, v, height, y, align='center', color=colors.pop())
            ax.text(y + v / 2.0, i, '$T_\mathrm{' + k + '}$', ha='center', va='center')
            xticks.append(y + v)
            xticks_labels.append('{:.1f}'.format(y + v))
            y += v
        yticks_labels.append('LD')
        yticks.append(i)

        ax.tick_params(axis='y', which='both', left='off', right='off')
        ax.tick_params(axis='x', which='both', top='off')
        ax.set_xlabel('t [cy]')
        ax.set_ylabel('execution port')
        ax.set_yticks(yticks)
        ax.set_yticklabels(yticks_labels)
        ax.set_xticks(xticks)
        ax.set_xticklabels(xticks_labels, rotation='vertical')
        ax.xaxis.grid(alpha=0.7, linestyle='--')
        fig.savefig(self._args.ecm_plot)


#!/usr/bin/env python3
"""Helper functions to instrument assembly code for and analyze with IACA."""
# Version check
import sys
import re
import subprocess
import os
from copy import copy
import argparse
from pprint import pformat
import textwrap
from collections import OrderedDict

from distutils.spawn import find_executable

from kerncraft import iaca_get
from . import __version__

# Within loop
START_MARKER = ['        movl      $111, %ebx # INSERTED BY KERNCRAFT IACA MARKER UTILITY\n'
                '        .byte     100        # INSERTED BY KERNCRAFT IACA MARKER UTILITY\n'
                '        .byte     103        # INSERTED BY KERNCRAFT IACA MARKER UTILITY\n'
                '        .byte     144        # INSERTED BY KERNCRAFT IACA MARKER UTILITY\n']
# After loop
END_MARKER = ['        movl      $222, %ebx # INSERTED BY KERNCRAFT IACA MARKER UTILITY\n'
              '        .byte     100        # INSERTED BY KERNCRAFT IACA MARKER UTILITY\n'
              '        .byte     103        # INSERTED BY KERNCRAFT IACA MARKER UTILITY\n'
              '        .byte     144        # INSERTED BY KERNCRAFT IACA MARKER UTILITY\n']

def strip_and_uncomment(asm_lines):
    """Strip whitespaces and comments from asm lines."""
    asm_stripped = []
    for line in asm_lines:
        # Strip comments and whitespaces
        asm_stripped.append(line.split('#')[0].strip())
    return asm_stripped

def strip_unreferenced_labels(asm_lines):
    """Strip all labels, which are never referenced."""
    asm_stripped = []
    for line in asm_lines:
        if re.match(r'^\S+:', line):
            # Found label
            label = line[0:line.find(':')]
            # Search for references to current label
            if not any([re.match(r'^[^#]*\s' + re.escape(label) + '[\s,]?.*$', l) for l in asm_lines]):
                # Skip labels without seen reference
                line = ''
        asm_stripped.append(line)
    return asm_stripped

def itemsEqual(lst):
   return lst[1:] == lst[:-1]

def find_asm_blocks(asm_lines):
    """Find blocks probably corresponding to loops in assembly."""
    blocks = []

    last_labels = OrderedDict()
    packed_ctr = 0
    avx_ctr = 0
    xmm_references = []
    ymm_references = []
    zmm_references = []
    gp_references = []
    mem_references = []
    increments = {}
    for i, line in enumerate(asm_lines):
        # Register access counts
        zmm_references += re.findall('%zmm[0-9]+', line)
        ymm_references += re.findall('%ymm[0-9]+', line)
        xmm_references += re.findall('%xmm[0-9]+', line)
        gp_references += re.findall('%r[a-z0-9]+', line)
        if re.search(r'\d*\(%\w+(,%\w+)?(,\d)?\)', line):
            m = re.search(r'(?P<off>[-]?\d*)\(%(?P<basep>\w+)(,%(?P<idx>\w+))?(?:,(?P<scale>\d))?\)'
                          r'(?P<eol>$)?',
                          line)
            mem_references.append((
                int(m.group('off')) if m.group('off') else 0,
                m.group('basep'),
                m.group('idx'),
                int(m.group('scale')) if m.group('scale') else 1,
                'load' if m.group('eol') is None else 'store'))

        if re.match(r"^[v]?(mul|add|sub|div|fmadd(132|213|231)?)[h]?p[ds]", line):
            if line.startswith('v'):
                avx_ctr += 1
            packed_ctr += 1
        elif re.match(r'^\S+:', line):
            # last_labels[label_name] = line_number
            last_labels[line[0:line.find(':')]] =i

            # Reset counters
            packed_ctr = 0
            avx_ctr = 0
            xmm_references = []
            ymm_references = []
            zmm_references = []
            gp_references = []
            mem_references = []
            increments = {}
        elif re.match(r'^inc[bwlq]?\s+%[a-z0-9]+', line):
            reg_start = line.find('%') + 1
            increments[line[reg_start:]] = 1
        elif re.match(r'^add[bwlq]?\s+\$[0-9]+,\s*%[a-z0-9]+', line):
            const_start = line.find('$') + 1
            const_end = line[const_start + 1:].find(',') + const_start + 1
            reg_start = line.find('%') + 1
            increments[line[reg_start:]] = int(line[const_start:const_end])
        elif re.match(r'^dec[bwlq]?', line):
            reg_start = line.find('%') + 1
            increments[line[reg_start:]] = -1
        elif re.match(r'^sub[bwlq]?\s+\$[0-9]+,', line):
            const_start = line.find('$') + 1
            const_end = line[const_start + 1:].find(',') + const_start + 1
            reg_start = line.find('%') + 1
            increments[line[reg_start:]] = -int(line[const_start:const_end])
        elif last_labels and re.match(r'^j[a-z]+\s+\S+\s*', line):
            # End of block(s) due to jump

            # Check if jump target matches any previously recoded label
            last_label = None
            last_label_line = -1
            for label_name, label_line in last_labels.items():
                if re.match(r'^j[a-z]+\s+' + re.escape(label_name) + r'\s*', line):
                    # matched
                    last_label = label_name
                    last_label_line = label_line

            labels = list(last_labels.keys())

            if last_label:
                # deduce loop increment from memory index register
                pointer_increment = None  # default -> can not decide, let user choose
                possible_idx_regs = None
                if mem_references:
                    # we found memory references to work with

                    # If store accesses exist, consider only those
                    store_references = [mref for mref in mem_references
                                        if mref[4] == 'store']
                    refs = store_references or mem_references

                    possible_idx_regs = list(set(increments.keys()).intersection(
                        set([r[1] for r in refs if r[1] is not None] +
                            [r[2] for r in refs if r[2] is not None])))
                    for mref in refs:
                        for reg in list(possible_idx_regs):
                            # Only consider references with two registers, where one could be an index
                            if None not in mref[1:3]:
                                # One needs to mach, other registers will be excluded
                                if not (reg == mref[1] or reg == mref[2]):
                                    # reg can not be it
                                    possible_idx_regs.remove(reg)

                    idx_reg = None
                    if len(possible_idx_regs) == 1:
                        # good, exactly one register was found
                        idx_reg = possible_idx_regs[0]
                    elif possible_idx_regs and itemsEqual([increments[pidxreg]
                                                           for pidxreg in possible_idx_regs]):
                        # multiple were option found, but all have the same increment
                        # use first match:
                        idx_reg = possible_idx_regs[0]

                    if idx_reg:
                        mem_scales = [mref[3] for mref in refs
                                      if idx_reg == mref[2] or idx_reg == mref[1]]

                        if itemsEqual(mem_scales):
                            # good, all scales are equal
                            try:
                                pointer_increment = mem_scales[0] * increments[idx_reg]
                            except:
                                print("labels", pformat(labels[labels.index(last_label):]))
                                print("lines", pformat(asm_lines[last_label_line:i + 1]))
                                print("increments", increments)
                                print("mem_references", pformat(mem_references))
                                print("idx_reg", idx_reg)
                                print("mem_scales", mem_scales)
                                raise

                blocks.append({'first_line': last_label_line,
                               'last_line': i,
                               'ops': i - last_label_line,
                               'labels': labels[labels.index(last_label):],
                               'packed_instr': packed_ctr,
                               'avx_instr': avx_ctr,
                               'XMM': (len(xmm_references), len(set(xmm_references))),
                               'YMM': (len(ymm_references), len(set(ymm_references))),
                               'ZMM': (len(zmm_references), len(set(zmm_references))),
                               'GP': (len(gp_references), len(set(gp_references))),
                               'regs': (len(xmm_references) + len(ymm_references) +
                                        len(zmm_references) + len(gp_references),
                                        len(set(xmm_references)) + len(set(ymm_references)) +
                                        len(set(zmm_references)) +
                                        len(set(gp_references))),
                               'pointer_increment': pointer_increment,
                               'lines': asm_lines[last_label_line:i + 1],
                               'possible_idx_regs': possible_idx_regs,
                               'mem_references': mem_references,
                               'increments': increments, })
            # Reset counters
            packed_ctr = 0
            avx_ctr = 0
            xmm_references = []
            ymm_references = []
            zmm_references = []
            gp_references = []
            mem_references = []
            increments = {}
            last_labels = OrderedDict()
    return list(enumerate(blocks))

def select_best_block(blocks):
    """Return best block selected based on simple heuristic."""
    # TODO make this cleverer with more stats
    if not blocks:
        raise ValueError("No suitable blocks were found in assembly.")
    best_block = max(blocks, key=lambda b: b[1]['packed_instr'])
    if best_block[1]['packed_instr'] == 0:
        best_block = max(blocks,
                         key=lambda b: (b[1]['ops'] + b[1]['packed_instr'] + b[1]['avx_instr'],
                                        b[1]['ZMM'], b[1]['YMM'], b[1]['XMM']))
    return best_block[0]

def userselect_increment(block):
    """Let user interactively select byte increment."""
    print("Selected block:")
    print('\n    ' + ('\n    '.join(block['lines'])))
    print()

    increment = None
    while increment is None:
        increment = input("Choose store pointer increment (number of bytes): ")
        try:
            increment = int(increment)
        except ValueError:
            increment = None

    block['pointer_increment'] = increment
    return increment

def userselect_block(blocks, default=None, debug=False):
    """Let user interactively select block."""
    print("Blocks found in assembly file:")
    print("      block     | OPs | pck. | AVX || Registers |    ZMM   |    YMM   |    XMM   |    GP   ||ptr.inc|\n"
          "----------------+-----+------+-----++-----------+----------+----------+----------+---------++-------|")
    for idx, b in blocks:
        print('{:>2} {b[labels]!r:>12} | {b[ops]:>3} | {b[packed_instr]:>4} | {b[avx_instr]:>3} |'
              '| {b[regs][0]:>3} ({b[regs][1]:>3}) | {b[ZMM][0]:>3} ({b[ZMM][1]:>2}) | '
              '{b[YMM][0]:>3} ({b[YMM][1]:>2}) | '
              '{b[XMM][0]:>3} ({b[XMM][1]:>2}) | {b[GP][0]:>2} ({b[GP][1]:>2}) || '
              '{b[pointer_increment]!s:>5} |'.format(idx, b=b))

        if debug:
            ln = b['first_line']
            print(' '*4 + 'Code:')
            for l in b['lines']:
                print(' '*8 + '{:>5} | {}'.format(ln, l))
                ln += 1
            print(' '*4 + 'Metadata:')
            print(textwrap.indent(
                pformat({k: v for k,v in b.items() if k not in ['lines']}),
                ' '*8))

    # Let user select block:
    block_idx = -1
    while not (0 <= block_idx < len(blocks)):
        block_idx = input("Choose block to be marked [" + str(default) + "]: ") or default
        try:
            block_idx = int(block_idx)
        except ValueError:
            block_idx = -1
    # block = blocks[block_idx][1]

    return block_idx

def insert_markers(asm_lines, start_line, end_line):
    """Insert IACA marker into list of ASM instructions at given indices."""
    asm_lines = (asm_lines[:start_line] + START_MARKER +
                 asm_lines[start_line:end_line + 1] + END_MARKER +
                 asm_lines[end_line + 1:])
    return asm_lines

def iaca_instrumentation(input_file, output_file,
                         block_selection='auto',
                         pointer_increment='auto_with_manual_fallback',
                         debug=False):
    """
    Add IACA markers to an assembly file.

    If instrumentation fails because loop increment could not determined automatically, a ValueError
    is raised.

    :param input_file: file-like object to read from
    :param output_file: file-like object to write to
    :param block_selection: index of the assembly block to instrument, or 'auto' for automatically
                            using block with the
                            most vector instructions, or 'manual' to read index to prompt user
    :param pointer_increment: number of bytes the pointer is incremented after the loop or
                              - 'auto': automatic detection, otherwise RuntimeError is raised
                              - 'auto_with_manual_fallback': like auto with fallback to manual input
                              - 'manual': prompt user
    :param debug: output additional internal analysis information. Only works with manual selection.
    :return: the instrumented assembly block
    """
    assembly_orig = input_file.readlines()

    # If input and output files are the same, overwrite with output
    if input_file is output_file:
        output_file.seek(0)
        output_file.truncate()

    if debug:
        block_selection = 'manual'

    assembly = strip_and_uncomment(copy(assembly_orig))
    assembly = strip_unreferenced_labels(assembly)
    blocks = find_asm_blocks(assembly)
    if block_selection == 'auto':
        block_idx = select_best_block(blocks)
    elif block_selection == 'manual':
        block_idx = userselect_block(blocks, default=select_best_block(blocks), debug=debug)
    elif isinstance(block_selection, int):
        block_idx = block_selection
    else:
        raise ValueError("block_selection has to be an integer, 'auto' or 'manual' ")

    block = blocks[block_idx][1]

    if pointer_increment == 'auto':
        if block['pointer_increment'] is None:
            raise RuntimeError("pointer_increment could not be detected automatically. Use "
                               "--pointer-increment to set manually to byte offset of store "
                               "pointer address between consecutive assembly block iterations.")
    elif pointer_increment == 'auto_with_manual_fallback':
        if block['pointer_increment'] is None:
            block['pointer_increment'] = userselect_increment(block)
    elif pointer_increment == 'manual':
        block['pointer_increment'] = userselect_increment(block)
    elif isinstance(pointer_increment, int):
        block['pointer_increment'] = pointer_increment
    else:
        raise ValueError("pointer_increment has to be an integer, 'auto', 'manual' or  "
                         "'auto_with_manual_fallback' ")

    instrumented_asm = insert_markers(assembly_orig, block['first_line'], block['last_line'])
    output_file.writelines(instrumented_asm)

    return block

def iaca_analyse_instrumented_binary(instrumented_binary_file, micro_architecture):
    """
    Run IACA analysis on an instrumented binary.

    :param instrumented_binary_file: path of binary that was built with IACA markers
    :param micro_architecture: micro architecture string as taken by IACA.
                               one of: NHM, WSM, SNB, IVB, HSW, BDW
    :return: a dictionary with the following keys:
        - 'output': the output of the iaca executable
        - 'throughput': the block throughput in cycles for one possibly vectorized loop iteration
        - 'port cycles': dict, mapping port name to number of active cycles
        - 'uops': total number of Uops
    """
    # Select IACA version and executable based on micro_architecture:
    arch_map = {
        # arch: (binary name, version string, required additional arguments)
        'NHM': ('iaca2.2', 'v2.2', ['-64']),
        'WSM': ('iaca2.2', 'v2.2', ['-64']),
        'SNB': ('iaca2.3', 'v2.3', ['-64']),
        'IVB': ('iaca2.3', 'v2.3', ['-64']),
        'HSW': ('iaca3.0', 'v3.0', []),
        'BDW': ('iaca3.0', 'v3.0', []),
        'SKL': ('iaca3.0', 'v3.0', []),
        'SKX': ('iaca3.0', 'v3.0', []),
    }

    if micro_architecture not in arch_map:
        raise ValueError('Invalid micro_architecture selected ({}), valid options are {}'.format(
            micro_architecture, ', '.join(arch_map.keys())))

    iaca_path = iaca_get.find_iaca()  # Throws exception if not found
    os.environ['PATH'] += ':' + iaca_path

    iaca_exec, iaca_version, base_args = arch_map[micro_architecture]
    if find_executable(iaca_exec) is None:
        raise RuntimeError("{0} executable was not found. Make sure that {0} is found in "
                           "{1}. Install using iaca_get.".format(iaca_exec, iaca_path))

    result = {}

    cmd = [iaca_exec] + base_args + ['-arch', micro_architecture, instrumented_binary_file]
    try:
        iaca_output = subprocess.check_output(cmd).decode('utf-8')
        result['output'] = iaca_output
    except OSError as e:
        raise RuntimeError("IACA execution failed:" + ' '.join(cmd) + '\n' + str(e))
    except subprocess.CalledProcessError as e:
        raise RuntimeError("IACA throughput analysis failed:" + str(e))

    # Get total cycles per loop iteration
    match = re.search(r'^Block Throughput: ([0-9.]+) Cycles', iaca_output, re.MULTILINE)
    assert match, "Could not find Block Throughput in IACA output."
    throughput = float(match.groups()[0])
    result['throughput'] = throughput

    # Find ports and cycles per port
    ports = [l for l in iaca_output.split('\n') if l.startswith('|  Port  |')]
    cycles = [l for l in iaca_output.split('\n') if l.startswith('| Cycles |')]
    assert ports and cycles, "Could not find ports/cycles lines in IACA output."
    ports = [p.strip() for p in ports[0].split('|')][2:]
    cycles = [c.strip() for c in cycles[0].split('|')][2:]
    port_cycles = []
    for i in range(len(ports)):
        if '-' in ports[i] and ' ' in cycles[i]:
            subports = [p.strip() for p in ports[i].split('-')]
            subcycles = [c for c in cycles[i].split(' ') if bool(c)]
            port_cycles.append((subports[0], float(subcycles[0])))
            port_cycles.append((subports[0] + subports[1], float(subcycles[1])))
        elif ports[i] and cycles[i]:
            port_cycles.append((ports[i], float(cycles[i])))
    result['port cycles'] = dict(port_cycles)

    match = re.search(r'^Total Num Of Uops: ([0-9]+)', iaca_output, re.MULTILINE)
    assert match, "Could not find Uops in IACA output."
    result['uops'] = float(match.groups()[0])
    return result

def main():
    """Execute command line interface."""
    parser = argparse.ArgumentParser(
        description='Find and analyze basic loop blocks and mark for IACA.',
        epilog='For help, examples, documentation and bug reports go to:\nhttps://github.com'
               '/RRZE-HPC/kerncraft\nLicense: AGPLv3')
    parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__))
    parser.add_argument('source', type=argparse.FileType(), nargs='?', default=sys.stdin,
                        help='assembly file to analyze (default: stdin)')
    parser.add_argument('--outfile', '-o', type=argparse.FileType('w'), nargs='?',
                        default=sys.stdout, help='output file location (default: stdout)')
    parser.add_argument('--debug', action='store_true',
                        help='Output nternal analysis information for debugging.')
    args = parser.parse_args()

    # pointer_increment is given, since it makes no difference on the command lien and requires
    # less user input
    iaca_instrumentation(input_file=args.source, output_file=args.outfile,
                         block_selection='manual', pointer_increment=1, debug=args.debug)

if __name__ == '__main__':
    main()