Repository: spack/spack

Issue:
Add microarchitecture specific flags for LLVM compilers
As a user I want to have microarchitecture specific flags injected for LLVM compilers so that I would compile by default code that is optimized for the target of the installation.

### Rationale

We already support this feature for GCC since #3206 and need to extend the capability to other compilers as well

### Description
What needs to be done is carefully reviewing docs and man pages, and encode in [this file](https://github.com/spack/spack/blob/develop/lib/spack/llnl/util/cpu/microarchitectures.json) the information found.



### Additional information
A non exhaustive list of pages we might want to consult:
- Docs linked in the official [Download page](https://releases.llvm.org/download.html)

We also need to check what will be the Fortran support for clang and if `flang` is compatible with all of the `-march` flags.



Discussion:
@alalazo would this change require #12011?
I wouldn't say it requires that, but they are related. Currently we could probably distinguish the two by the version suffix.
The optimization flags for LLVM seem to be of the form:
```
-march=<family> -mcpu=<microarchitecture>
```
more info on the assembler [doc page](https://llvm.org/docs/CommandGuide/llc.html). This means that a mixed toolchain with LLVM and GCC won't be supported for free under the current model since the flags are different.
Damn, LLVM + GCC is very common, especially on macOS. Maybe it's time we figure out a better way to handle mixed compiler support.

Resolving patch:
diff --git a/lib/spack/llnl/util/cpu/microarchitecture.py b/lib/spack/llnl/util/cpu/microarchitecture.py
--- a/lib/spack/llnl/util/cpu/microarchitecture.py
+++ b/lib/spack/llnl/util/cpu/microarchitecture.py
@@ -224,7 +224,8 @@ def satisfies_constraint(entry, version):
             version, _, suffix = version.partition('-')
 
             # If the suffixes are not all equal there's no match
-            if suffix != min_suffix or suffix != max_suffix:
+            if ((suffix != min_suffix and min_version) or
+                (suffix != max_suffix and max_version)):
                 return False
 
             # Assume compiler versions fit into semver
diff --git a/lib/spack/spack/architecture.py b/lib/spack/spack/architecture.py
--- a/lib/spack/spack/architecture.py
+++ b/lib/spack/spack/architecture.py
@@ -58,6 +58,7 @@
 """
 import functools
 import inspect
+import warnings
 
 import six
 
@@ -184,6 +185,21 @@ def __contains__(self, cpu_flag):
         return cpu_flag in self.microarchitecture
 
     def optimization_flags(self, compiler):
+        """Returns the flags needed to optimize for this target using
+        the compiler passed as argument.
+
+        Args:
+            compiler (CompilerSpec or Compiler): object that contains both the
+                name and the version of the compiler we want to use
+        """
+        if isinstance(compiler, spack.compiler.Compiler):
+            if spack.compilers.is_mixed_toolchain(compiler):
+                msg = ('microarchitecture specific optimizations are not '
+                       'supported yet on mixed compiler toolchains [check'
+                       ' {0.name}@{0.version} for further details]')
+                warnings.warn(msg.format(compiler))
+                return ''
+
         return self.microarchitecture.optimization_flags(
             compiler.name, str(compiler.version)
         )
diff --git a/lib/spack/spack/compilers/__init__.py b/lib/spack/spack/compilers/__init__.py
--- a/lib/spack/spack/compilers/__init__.py
+++ b/lib/spack/spack/compilers/__init__.py
@@ -676,6 +676,44 @@ def _default(cmp_id, paths):
     return compilers
 
 
+def is_mixed_toolchain(compiler):
+    """Returns True if the current compiler is a mixed toolchain,
+    False otherwise.
+
+    Args:
+        compiler (Compiler): a valid compiler object
+    """
+    cc = os.path.basename(compiler.cc or '')
+    cxx = os.path.basename(compiler.cxx or '')
+    f77 = os.path.basename(compiler.f77 or '')
+    fc = os.path.basename(compiler.fc or '')
+
+    toolchains = set()
+    for compiler_cls in all_compiler_types():
+        # Inspect all the compiler toolchain we know. If a compiler is the
+        # only compiler supported there it belongs to that toolchain.
+        def name_matches(name, name_list):
+            # This is such that 'gcc' matches variations
+            # like 'ggc-9' etc that are found in distros
+            name, _, _ = name.partition('-')
+            return len(name_list) == 1 and name and name in name_list
+
+        if any([
+            name_matches(cc, compiler_cls.cc_names),
+            name_matches(cxx, compiler_cls.cxx_names),
+            name_matches(f77, compiler_cls.f77_names),
+            name_matches(fc, compiler_cls.fc_names)
+        ]):
+            tty.debug("[TOOLCHAIN] MATCH {0}".format(compiler_cls.__name__))
+            toolchains.add(compiler_cls.__name__)
+
+    if len(toolchains) > 1:
+        tty.debug("[TOOLCHAINS] {0}".format(toolchains))
+        return True
+
+    return False
+
+
 class InvalidCompilerConfigurationError(spack.error.SpackError):
 
     def __init__(self, compiler_spec):


Test patch:
diff --git a/lib/spack/spack/test/architecture.py b/lib/spack/spack/test/architecture.py
--- a/lib/spack/spack/test/architecture.py
+++ b/lib/spack/spack/test/architecture.py
@@ -169,3 +169,21 @@ def test_target_container_semantic(cpu_flag, target_name):
 def test_arch_spec_container_semantic(item, architecture_str):
     architecture = spack.spec.ArchSpec(architecture_str)
     assert item in architecture
+
+
+@pytest.mark.parametrize('compiler_spec,target_name,expected_flags', [
+    # Check compilers with version numbers from a single toolchain
+    ('gcc@4.7.2', 'haswell', '-march=core-avx2 -mtune=core-avx2'),
+    # Check mixed toolchains
+    ('clang@8.0.0', 'broadwell', ''),
+    # Check clang compilers with 'apple' suffix
+    ('clang@9.1.0-apple', 'x86_64', '-march=x86-64 -mcpu=generic')
+])
+@pytest.mark.filterwarnings("ignore:microarchitecture specific")
+def test_optimization_flags(
+        compiler_spec, target_name, expected_flags, config
+):
+    target = spack.architecture.Target(target_name)
+    compiler = spack.compilers.compilers_for_spec(compiler_spec).pop()
+    opt_flags = target.optimization_flags(compiler)
+    assert opt_flags == expected_flags
diff --git a/lib/spack/spack/test/compilers.py b/lib/spack/spack/test/compilers.py
--- a/lib/spack/spack/test/compilers.py
+++ b/lib/spack/spack/test/compilers.py
@@ -469,3 +469,11 @@ def test_cce_version_detection(version_str, expected_version):
 def test_fj_version_detection(version_str, expected_version):
     version = spack.compilers.fj.Fj.extract_version_from_output(version_str)
     assert version == expected_version
+
+
+@pytest.mark.parametrize('compiler_spec,expected_result', [
+    ('gcc@4.7.2', False), ('clang@3.3', False), ('clang@8.0.0', True)
+])
+def test_detecting_mixed_toolchains(compiler_spec, expected_result, config):
+    compiler = spack.compilers.compilers_for_spec(compiler_spec).pop()
+    assert spack.compilers.is_mixed_toolchain(compiler) is expected_result
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -585,7 +585,9 @@ def test_noversion_pkg(self, spec):
 
     @pytest.mark.parametrize('spec, best_achievable', [
         ('mpileaks%gcc@4.8', 'haswell'),
-        ('mpileaks%gcc@5.3.0', 'skylake_avx512')
+        ('mpileaks%gcc@5.3.0', 'skylake_avx512'),
+        # Apple's clang always falls back to x86-64 for now
+        ('mpileaks%clang@9.1.0-apple', 'x86_64')
     ])
     def test_adjusting_default_target_based_on_compiler(
             self, spec, best_achievable, current_host
diff --git a/lib/spack/spack/test/data/compilers.yaml b/lib/spack/spack/test/data/compilers.yaml
--- a/lib/spack/spack/test/data/compilers.yaml
+++ b/lib/spack/spack/test/data/compilers.yaml
@@ -114,3 +114,24 @@ compilers:
       cflags: -O3
       cxxflags: -O3
     modules: 'None'
+- compiler:
+    spec: clang@8.0.0
+    operating_system: redhat7
+    paths:
+      cc: /path/to/clang-8
+      cxx: /path/to/clang++-8
+      f77: /path/to/gfortran-9
+      fc: /path/to/gfortran-9
+    flags:
+      cflags: -O3
+      cxxflags: -O3
+    modules: 'None'
+- compiler:
+    spec: clang@9.1.0-apple
+    operating_system: elcapitan
+    paths:
+      cc: /path/to/clang
+      cxx: /path/to/clang++
+      f77: None
+      fc: None
+    modules: 'None'
diff --git a/lib/spack/spack/test/llnl/util/cpu.py b/lib/spack/spack/test/llnl/util/cpu.py
--- a/lib/spack/spack/test/llnl/util/cpu.py
+++ b/lib/spack/spack/test/llnl/util/cpu.py
@@ -191,6 +191,7 @@ def test_target_json_schema():
 
 
 @pytest.mark.parametrize('target_name,compiler,version,expected_flags', [
+    # Test GCC
     ('x86_64', 'gcc', '4.9.3', '-march=x86-64 -mtune=generic'),
     ('x86_64', 'gcc', '4.2.0', '-march=x86-64 -mtune=generic'),
     ('x86_64', 'gcc', '4.1.1', '-march=x86-64 -mtune=x86-64'),
@@ -198,6 +199,12 @@ def test_target_json_schema():
     ('nehalem', 'gcc', '4.9.3', '-march=nehalem -mtune=nehalem'),
     ('nehalem', 'gcc', '4.8.5', '-march=corei7 -mtune=corei7'),
     ('sandybridge', 'gcc', '4.8.5', '-march=corei7-avx -mtune=corei7-avx'),
+    # Test Clang / LLVM
+    ('sandybridge', 'clang', '3.9.0', '-march=x86-64 -mcpu=sandybridge'),
+    ('icelake', 'clang', '6.0.0', '-march=x86-64 -mcpu=icelake'),
+    ('icelake', 'clang', '8.0.0', '-march=x86-64 -mcpu=icelake-client'),
+    ('zen2', 'clang', '9.0.0', '-march=x86-64 -mcpu=znver2'),
+    ('power9le', 'clang', '8.0.0', '-march=ppc64le -mcpu=pwr9'),
     # Test that an unknown compiler returns an empty string
     ('sandybridge', 'unknown', '4.8.5', ''),
 ])