This method does the actual work of _get_cpu_cores_per_run
    without reading the machine architecture from the file system
    in order to be testable. For description, c.f. above.
    Note that this method might change the input parameters!
    Do not call it directly, call getCpuCoresPerRun()!
    @param allCpus: the list of all available cores
    @param cores_of_package: a mapping from package (CPU) ids to lists of cores that belong to this CPU
    @param siblings_of_core: a mapping from each core to a list of sibling cores including the core itself (a sibling is a core sharing the same physical core)

def _get_cpu_cores_per_run0(coreLimit, num_of_threads, allCpus, cores_of_package, siblings_of_core):
    """This method does the actual work of _get_cpu_cores_per_run
    without reading the machine architecture from the file system
    in order to be testable. For description, c.f. above.
    Note that this method might change the input parameters!
    Do not call it directly, call getCpuCoresPerRun()!
    @param allCpus: the list of all available cores
    @param cores_of_package: a mapping from package (CPU) ids to lists of cores that belong to this CPU
    @param siblings_of_core: a mapping from each core to a list of sibling cores including the core itself (a sibling is a core sharing the same physical core)
    """
    # First, do some checks whether this algorithm has a chance to work.
    if coreLimit > len(allCpus):
        sys.exit("Cannot run benchmarks with {0} CPU cores, only {1} CPU cores available.".format(coreLimit, len(allCpus)))
    if coreLimit * num_of_threads > len(allCpus):
        sys.exit("Cannot run {0} benchmarks in parallel with {1} CPU cores each, only {2} CPU cores available. Please reduce the number of threads to {3}.".format(num_of_threads, coreLimit, len(allCpus), len(allCpus) // coreLimit))

    package_size = None # Number of cores per package
    for package, cores in cores_of_package.items():
        if package_size is None:
            package_size = len(cores)
        elif package_size != len(cores):
            sys.exit("Asymmetric machine architecture not supported: CPU package {0} has {1} cores, but other package has {2} cores.".format(package, len(cores), package_size))

    core_size = None # Number of threads per core
    for core, siblings in siblings_of_core.items():
        if core_size is None:
            core_size = len(siblings)
        elif core_size != len(siblings):
            sys.exit("Asymmetric machine architecture not supported: CPU core {0} has {1} siblings, but other core has {2} siblings.".format(core, len(siblings), core_size))

    all_cpus_set = set(allCpus)
    for core, siblings in siblings_of_core.items():
        siblings_set = set(siblings)
        if not siblings_set.issubset(all_cpus_set):
            sys.exit("Core assignment is unsupported because siblings {0} of core {1} are not usable. Please always make all virtual cores of a physical core available.".format(siblings_set.difference(all_cpus_set), core))

    # Second, compute some values we will need.
    package_count = len(cores_of_package)
    packages = sorted(cores_of_package.keys())
    coreLimit_rounded_up = int(math.ceil(coreLimit / core_size) * core_size)
    assert coreLimit <= coreLimit_rounded_up < (coreLimit + core_size)

    packages_per_run = int(math.ceil(coreLimit_rounded_up / package_size))
    if packages_per_run > 1 and packages_per_run * num_of_threads > package_count:
        sys.exit("Cannot split runs over multiple CPUs and at the same time assign multiple runs to the same CPU. Please reduce the number of threads to {0}.".format(package_count // packages_per_run))

    runs_per_package = int(math.ceil(num_of_threads / package_count))
    assert packages_per_run == 1 or runs_per_package == 1
    if packages_per_run == 1 and runs_per_package * coreLimit > package_size:
        sys.exit("Cannot run {} benchmarks with {} cores on {} CPUs with {} cores, because runs would need to be split across multiple CPUs. Please reduce the number of threads.".format(num_of_threads, coreLimit, package_count, package_size))

    # Warn on misuse of hyper-threading
    need_HT = False
    if packages_per_run == 1:
        # Checking whether the total amount of usable physical cores is not enough,
        # there might be some cores we cannot use, e.g. when scheduling with coreLimit=3 on quad-core machines.
        # Thus we check per package.
        assert coreLimit * runs_per_package <= package_size
        if coreLimit_rounded_up * runs_per_package > package_size:
            need_HT = True
            logging.warning("The number of threads is too high and hyper-threading sibling cores need to be split among different runs, which makes benchmarking unreliable. Please reduce the number of threads to %s.", (package_size // coreLimit_rounded_up) * package_count)

    else:
        if coreLimit_rounded_up * num_of_threads > len(allCpus):
            assert coreLimit_rounded_up * runs_per_package > package_size
            need_HT = True
            logging.warning("The number of threads is too high and hyper-threading sibling cores need to be split among different runs, which makes benchmarking unreliable. Please reduce the number of threads to %s.", len(allCpus) // coreLimit_rounded_up)

    logging.debug("Going to assign at most %s runs per package, each one using %s cores and blocking %s cores on %s packages.", runs_per_package, coreLimit, coreLimit_rounded_up, packages_per_run)

    # Third, do the actual core assignment.
    result = []
    used_cores = set()
    for run in range(num_of_threads):
        # this calculation ensures that runs are split evenly across packages
        start_package = (run * packages_per_run) % package_count
        cores = []
        cores_with_siblings = set()
        for package_nr in range(start_package, start_package + packages_per_run):
            assert len(cores) < coreLimit
            # Some systems have non-contiguous package numbers,
            # so we take the i'th package out of the list of available packages.
            # On normal system this is the identity mapping.
            package = packages[package_nr]
            for core in cores_of_package[package]:
                if core not in cores:
                    cores.extend(c for c in siblings_of_core[core] if not c in used_cores)
                if len(cores) >= coreLimit:
                    break
            cores_with_siblings.update(cores)
            cores = cores[:coreLimit] # shrink if we got more cores than necessary
            # remove used cores such that we do not try to use them again
            cores_of_package[package] = [core for core in cores_of_package[package] if core not in cores]

        assert len(cores) == coreLimit, "Wrong number of cores for run {} of {} - previous results: {}, remaining cores per package: {}, current cores: {}".format(run+1, num_of_threads, result, cores_of_package, cores)
        blocked_cores = cores if need_HT else cores_with_siblings
        assert not used_cores.intersection(blocked_cores)
        used_cores.update(blocked_cores)
        result.append(sorted(cores))

    assert len(result) == num_of_threads
    assert all(len(cores) == coreLimit for cores in result)
    assert len(set(itertools.chain(*result))) == num_of_threads * coreLimit, "Cores are not uniquely assigned to runs: " + result

    logging.debug("Final core assignment: %s.", result)
    return result