This is an archive of the discontinued LLVM Phabricator instance.

[C++20] [Modules] Implement discardable decls in GMF in ASTWriter
Needs ReviewPublic

Authored by ChuanqiXu on Jul 10 2023, 8:05 PM.

Details

Summary

This is an alternative of https://reviews.llvm.org/D126694,
which is originally developed by Iain Sandoe <iain@sandoe.co.uk>

This patch intends to implement the feature to discard unreachable
decls in the global module fragment. The formal wording lives in
[[module.global.frag]](http://eel.is/c++draft/module.global.frag).

Informally, the feature means that we can discard the not (transitively)
used/referenced decls in the global module fragment. This is important
since many declarations in the headers won't be used in practice. So that we
can save spaces. Also it is helpful for the compilation speed since it is possible that
we can avoid handling redecorations from different modules, which is a major blocking
issue for the performance of C++20 named modules.

To achieve this, we need to implement this in the serialization side instead of deserialization side.
Otherwise, the feature won't be helpful for the size and the speed.

Different from https://reviews.llvm.org/D126694, this patch tries to mark the discardable decls (in the GMF) within
the process of writing BMIs instead of marking them with a manual yet-another AST Visitors. The reason is:

  • It is a natural process for AST Writers. The part that ASTWriters don't write is naturally unreachable.
  • From the engineering perspective, it looks bad to repeat the AST Visitors again. There are so many codes in ASTWriters. Also it may be slow to visit the AST again with a standalone traversal.

Now the patch can pass all the in-tree tests. But I meet some ODR violations after I apply this to some real world projects. So I need to resolve that first.


Performance

libcxx std module

Tested with std module from libcxx: https://libcxx.llvm.org/Modules.html
(commit version: 7aebe4eaaa10155dc3c3619)

sizebuild time
Original432M3.8s
Discarding335M3.2s

The numbers show that the feature can save at least 22% size and 15% building time.

use std module

For this example:

import std;

int main(int, char**) {
  std::vector<int> v;
  v.push_back(5);
  v.push_back(7);
  v.push_back(2);
  v.push_back(7);
  v.push_back(4);
  v.push_back(1);

  int t{3};
  std::optional<int> op{3};
  v.push_back((op <=> t) == std::strong_ordering::equal);

  std::sort(v.begin(), v.end());
  for (int i : v)
    std::cout << i << " ";
  std::cout << "\n";
  return 0;
}

The compilation speed is:

build time
Original3.395s
Discarding2.455s
non-modular code1.861s

So that we can get a significant win with discarding approach.

(ps: the testing non modular version is:

// import std;
#include <vector>
#include <optional>
#include <algorithm>
#include <iostream>

int main(int, char**) {
  
  std::vector<int> v;
  v.push_back(5);
  v.push_back(7);
  v.push_back(2);
  v.push_back(7);
  v.push_back(4);
  v.push_back(1);

  int t{3};
  std::optional<int> op{3};
  v.push_back((op <=> t) == std::strong_ordering::equal);

  std::sort(v.begin(), v.end());
  for (int i : v)
    std::cout << i << " ";
  std::cout << "\n";
  return 0;
}

)

pr61477

This comes from https://github.com/llvm/llvm-project/issues/61447

The pattern of the issue is:

/// vulkan-a.cppm ... vulkan-z.cppm
module;
#include <iostream>
// ${part} comes from a..z
export module vulkan:${part};

export namespace ${part} {
    void hello() {
        std::cout << "hello\n";
    }
}

// vulkan.cppm
export module vulkan;
export import :${part};

// test.cppm
module;
#include <iostream>
export module test;
import vulkan; // Vulcan is a constructed big module.

In this case, module vulcan is a constructed big module and it shows that we will take many times to handle the redeclaration due to the requirement of the languages. (It is not the topic of the patch).

sizebuild time
Original164M5.2s
Discarding118M3.8s

In this case, the feature can save 28% space and 26% building times.

Diff Detail

Event Timeline

ChuanqiXu created this revision.Jul 10 2023, 8:05 PM
Herald added a project: Restricted Project. · View Herald TranscriptJul 10 2023, 8:05 PM
ChuanqiXu requested review of this revision.Jul 10 2023, 8:05 PM
ChuanqiXu updated this revision to Diff 538902.Jul 10 2023, 8:25 PM

Code cleanups

ChuanqiXu edited the summary of this revision. (Show Details)Jul 10 2023, 8:40 PM
ChuanqiXu edited the summary of this revision. (Show Details)Jul 10 2023, 8:47 PM
ChuanqiXu updated this revision to Diff 538908.Jul 10 2023, 9:33 PM
ChuanqiXu edited the summary of this revision. (Show Details)

Code cleanups.

ChuanqiXu edited the summary of this revision. (Show Details)Jul 10 2023, 10:46 PM
ChuanqiXu edited the summary of this revision. (Show Details)Jul 10 2023, 11:00 PM
ChuanqiXu edited the summary of this revision. (Show Details)Jul 10 2023, 11:03 PM
ChuanqiXu updated this revision to Diff 541814.Jul 18 2023, 7:19 PM
ChuanqiXu retitled this revision from [WIP] [C++20] [Modules] Implement discardable decls in GMF in ASTWriter to [C++20] [Modules] Implement discardable decls in GMF in ASTWriter.
ChuanqiXu added a subscriber: rsmith.

Update: While this patch can pass all the in-tree tests early, it had problems with real projects. The problems are the false positive ODR violations. I think the root cause of the failures lives in the current ODR checking systems instead of the current patch. It looks like the current ODR checking systems are context sensitive. That said, the same type in different module units can be considered to be different types if some module units discard the unused declarations while other module units don't. This is incorrect or too aggressive.

In such situations, if we land this patch as-is, it may cause severe regressions to users. This is not acceptable. So we have roughly two paths here:

  1. Just put the patch here until we fix the underlying issue properly.
  2. Offer an by default off option to enable this feature. So that the volunteering users can have early experiences if they want or they can provide reduced/simpler reproducer to help us to process.

A tricky part of the feature (at least in my mind) is that the feature is not purely a compiler optimizations but a language features recorded in the spec formally. It has semantical impacts for users. (See https://eel.is/c++draft/module.global.frag#example-2 and clang/test/CXX/module/module.glob.frag/cxx20-10-4-ex2.cppm in the patch). So this is the feature that we definitely want/need to implement otherwise clang won't never conform the C++20 modules.

(I guess there will be opinions to implement the feature only in the semantical level first. e.g., implement this purely in Sema. But I think such directions violates the design intention. I believe the major motivation of http://eel.is/c++draft/module.global.frag#4 is about optimizations. Otherwise it is a pure lose for users. CC @rsmith. So I think this may not be an alternative)


Following off is more detailed description for the problems that I met. I met two problems when testing the feature:

  1. There is a wide used pattern in libstdc++:
namespace std
{
  inline namespace __cxx11 __attribute__((__abi_tag__ ("cxx11"))) { }
}
namespace __gnu_cxx
{
  inline namespace __cxx11 __attribute__((__abi_tag__ ("cxx11"))) { }
}
...
namespace std {
  some declarations for STL.
  ...
  namespace __cxx11 {
    some declarations for STL.
  }
}

Then in a real project, we observed false-positive ODR violations
since some module units discard __gnu_cxx::__cxx11 namespace while
other module units don't discard __gnu_cxx::__cxx11 namespace.

This may imply that the ODR checking process is context sensitive.
That said, the same type in different module units can be considered to be
different if some module units discard the unused __gnu_cxx::__cxx11 namespace
while other module units don't. This is incorrect.

In the current patch, I workaround this one by forcing to not discard namespaces with AbiTag. But this may not be correct. (We should fix the underlying problems.) So if we choose to land this with an by default off option, we need to revert the workaround.

  1. When we mixed the use of #include and imports with discarded declarations, we will meet ODR violations problems for some cases:
#include <vector>
#include <iostream>
import std;
... // use of STLs

In the module std, some declarations are discarded while in the current TU, everything included in <vector> and <iostream> is remained. Then some odd ODR violation happens.

@rsmith @v.g.vassilev hi, I am wondering if you have insights about the method and the false-positive result of profiling types.