-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Optimize Condition Resolution #6279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@swift-ci please smoke test. |
/// later by Condition Resolution. | ||
unsigned HasBeenResolved : 1; | ||
}; | ||
enum { NumIfConfigDeclBits = NumDeclBits + 3 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Off-by-one. Wonderful.
@@ -1291,6 +1291,12 @@ ASTContext::getModule(ArrayRef<std::pair<Identifier, SourceLoc>> ModulePath) { | |||
return M; | |||
|
|||
auto moduleID = ModulePath[0]; | |||
// Record this load request. If we've seen it before and couldn't produce a | |||
// loaded module it must have failed to load before. | |||
if (!RequestedModuleLoads.insert(moduleID.first).second) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to just use the existing LoadedModules map for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does ModuleDecl *
have a sensible sentinel value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Identifier isn't the one that needs the sentinel; it's a map from Identifier to ModuleDecl *.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha. Well, that would go through a pointer's dense map info so... I'm not entirely sure what would work here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
llvm::MapVector<Identifier, ModuleDecl*> LoadedModules;
If moduleID.first
is in the map, but the value is nullptr
, then it must have failed to load. (This would require changing some of the other places that use LoadedModules too.)
0d0dc31
to
5f6e545
Compare
I've withdrawn the parts of this pull request that deal with optimizing the module loading behavior. It turns out there's some trickiness there around loading modulemap-based modules. I'll try to take a crack at it later. @swift-ci please smoke test. |
return | ||
Context.getModule({ { argumentIdent, UDRE->getLoc() } }) != nullptr; | ||
|
||
auto *M = Context.getModule({ { argumentIdent, UDRE->getLoc() } }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In retrospect this seems terrifying; loading a module has side effects even when it isn't imported. (Extensions still get hooked up, AnyObject dynamic lookup still finds things, operators still become visible.) All of those things are bugs, but we should probably put a canImport
operation on ModuleLoader. (That'd be cheaper anyway if you didn't actually end up loading the thing.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. To what extent do we need to load a module to figure out if it is "importable?" It doesn't seem enough to search for it on disk, and it might depend on further modules that fail to load themselves.
OK, so there are three major cases to consider for the definition of 'Importable' here.
Modules still have to be imported by the
Modules get run through (required) validation to report rich errors about failures. "Importable" here probably means "passes this validation", but that can be expensive for repeated requests.
The hard one. Invokes the typechecker to get diagnostics in the dependent file, so probably need to add a new flag to the typechecker for a "dry run" or something to just see if the thing passes. Though, we might be able to get by with not running the type checker and just validating that we can load the file at all. The subtext here is: does "I can import this" really mean "I can import this without error"? |
@CodaFi Since you're working on this area, a few compiler_crashers are failing with various IfConfigStmt-related assertions. Might be an easy fix for you. |
FWIW, we don't actually care about SourceLoader. If we ever design a Python-like module system that lets us load from source, we'll build it from scratch to do some kind of subprocess compilation, not continue to make SourceLoader limp along. For the ClangImporter, there may be Clang APIs we can use to determine if a module exists and can be imported without actually loading it, even as hidden. For SerializedModuleLoader, I think we can just do enough to detect that the module exists; this mechanism isn't supposed to be used for fallbacks like "oh, one of my dependencies is missing". |
At least, that's my understanding. @erica, without asking you to go read all the kinds of module loading we support, what behavior would you expect for a module that happens to exist on disk, but would fail some time in the loading process? It seems like checking for this in |
Here from tweet (http://twitter.com/CodaFi_/status/810709572956647424) Putting kids to bed. My interpretation is if if the compiler emits "No such module (name)", it fails. Email notifications from github seem borked and intermittent right now. Don't know why. If you don't see me respond to a name ping, please email me directly. @CodaFi @jrose-apple. I caught the tweet by accident. I'll stick around for the next 30 minutes if you need me. |
Thanks for taking the time. Currently, that diagnostic is emitted late in the module loading phase. To take a fairly uncommon example in practice, but one relevant to something like the Swift Standard Library: What should the result checking a version of the |
@CodaFi What I believe you're asking is: how should Stepping back. The discussion for this proposal grew from platform differentiation, specifically for allowing module-dependencies to direct code. On iOS, watchOS, tvOS, you can use UIKit to perform identical tasks. Not all tasks but there's a basic core UIKit whose APIs are guaranteed to work across all three platforms. This test enables you to differentiate this code from code that run with Cocoa. Using modules lets you establish an API contract rather than testing for platform qualities. You write:
If you cannot import it for any reason, the API is unavailable, and the code should be excluded.
|
Ideally, a test for You can work around this in an extremely ugly way using Swift features that doesn't exist:
|
5f6e545
to
9c8683a
Compare
I don't think it's right to require protocol MyProtocol {}
#if canImport(Foundation)
// On platforms which don't support Foundation, this conformance isn't meaningful
import Foundation
extension Data: MyProtocol {}
#endif I think that's a reasonably common use-case. You shouldn't have to have an 'else' block and manually silence a compiler warning. |
Hm. I agree with this in theory—you shouldn't be asking the question if the subsequent import is going to break—but at the same time I'd still like to not do all the work of loading a module just to be sure the error occurs. I guess it's rare enough that you'd want to check for the presence of a module but not load it that we don't need to worry for now, as long as the error still gets emitted. I agree with @karwa that |
Got to sleep on this overnight.
If I understand this correctly, whether the import is successful or not, the test is for "would The scenario I want to specifically avoid is this: #if canImport(RoseKit)
// something that isn't included because RoseKit was compiled for Swift 1.3
#endif This should not silently exclude code because RoseKit is compiled for the wrong version of Swift. Something has to complain, I don't know where. If Swift "fails" here, I'd prefer it to fail by including the code and incurring failures later in the compilation process rather than excluding it and having that code be omitted without notice. I'm thinking that so long as RoseKit is recognized as an available module, no matter whether it's reasonable to import it or not, the test will succeed if you have to allow/disallow earlier in the process than the point at which you decide whether the module is valid or invalid.
Again, you guys all know the implementation details better than I do. |
Ah, yes, it definitely shouldn't silently skip the body of the |
Fantastic, that should speed things up quite a bit. I'll update this pull request soon. |
9c8683a
to
1ae4597
Compare
|
||
// Now load the top-level module, so that we can check if the submodule | ||
// exists without triggering a fatal error. | ||
return (result != nullptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure of whether this can all be replaced by
Impl.Instance->getPreprocessor().getHeaderSearchInfo().lookupModule(path)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definitely seems interesting. @benlangmuir?
If not, I think we need to factor this out in some way to share code. Also, does this do the right thing for submodules?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
canImport
does not work with submodules at the moment. I can make it work and see what happens.
08f9beb
to
3b374c4
Compare
@swift-ci please smoke test. |
@jrose-apple Is this still good to go? |
/// \brief Check whether the module with a given name can be imported without | ||
/// importing it. | ||
/// | ||
/// Note that if errors occur during the module loading process this check may |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not quite the condition we're going for. It's more "even if this check succeeds, errors may still occur if the module is eventually loaded", right?
|
||
// Now load the top-level module, so that we can check if the submodule | ||
// exists without triggering a fatal error. | ||
return (result != nullptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definitely seems interesting. @benlangmuir?
If not, I think we need to factor this out in some way to share code. Also, does this do the right thing for submodules?
return false; | ||
} | ||
} | ||
return (moduleInputBuffer != nullptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably worth avoiding the construction of the llvm::MemoryBuffers. Maybe we should be passing pointers instead of references?
class UIKitView : UIView {} // expected-error {{use of undeclared type 'UIView'}} | ||
#endif | ||
|
||
#if canImport(CoreGraphics) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should have some tests for Swift modules that aren't overlays and for Objective-C modules that don't have overlays, because I don't trust overlays to do normal things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still applies.
A bit more detailed feedback for the more lazy approach.
3b374c4
to
14f5251
Compare
0c7297c
to
25f2cc5
Compare
bool ClangImporter::canImportModule(std::pair<Identifier, SourceLoc> moduleID) { | ||
// Look up the top-level module to see if it exists. | ||
// FIXME: This only works with top-level modules. To support submodules, this | ||
// should be replaced by checking the result of `loadModule`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is the correct planned fix, but since we're only allowing users to check top-level modules for now it seems like an acceptable limitation. Maybe just delete the "To support submodules" part?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all remaining comments are nitpicks. :-)
// should be replaced by checking the result of `loadModule`. | ||
auto &clangHeaderSearch = Impl.getClangPreprocessor().getHeaderSearchInfo(); | ||
clang::Module *clangModule = | ||
clangHeaderSearch.lookupModule(moduleID.first.str()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still looking for confirmation from @benlangmuir that this is an acceptable way to check for the existence of a module without loading it.
// Try to open the module file first. If we fail, don't even look for the | ||
// module documentation file. | ||
Scratch.clear(); | ||
llvm::sys::path::append(Scratch, DirName, ModuleFilename); | ||
llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> ModuleOrErr = | ||
if (ModuleBuffer && ModuleDocBuffer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: Let's flip this around and make it an early return.
if (ModuleDocOrErr) | ||
*ModuleDocBuffer = std::move(ModuleDocOrErr.get()); | ||
} else { | ||
return llvm::sys::fs::access(StringRef(Scratch.data(), Scratch.size()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's shorthand for this: llvm::sys::fs::exists
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trying to get a std::error_code
return, not a bool
ean one.
// First see if we find it in the registered memory buffers. | ||
if (!MemoryBuffers.empty()) { | ||
// FIXME: Right now this works only with access paths of length 1. | ||
// Once submodules are designed, this needs to support suffix |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not assume anything about how submodules might work.
class UIKitView : UIView {} // expected-error {{use of undeclared type 'UIView'}} | ||
#endif | ||
|
||
#if canImport(CoreGraphics) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still applies.
25f2cc5
to
7c72f85
Compare
There. Barring comment by @benlangmuir @swift-ci please smoke test. |
7c72f85
to
5721a19
Compare
@swift-ci please smoke test. |
// FIXME: This only works with top-level modules. | ||
auto &clangHeaderSearch = Impl.getClangPreprocessor().getHeaderSearchInfo(); | ||
clang::Module *clangModule = | ||
clangHeaderSearch.lookupModule(moduleID.first.str()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the late comment, but this will only tell you that the module map was found and parsed. The module may still be unavailable for a variety of reasons (e.g. it has an unfulfilled "requires" declaration). You should also call the following method from clang to check that the module makes sense to import:
/// \brief Determine whether this module is available for use within the
/// current translation unit.
///
/// \param LangOpts The language options used for the current
/// translation unit.
///
/// \param Target The target options used for the current translation unit.
///
/// \param Req If this module is unavailable, this parameter
/// will be set to one of the requirements that is not met for use of
/// this module.
bool isAvailable(const LangOptions &LangOpts,
const TargetInfo &Target,
Requirement &Req,
UnresolvedHeaderDirective &MissingHeader) const;
I think you can just ignore the out parameters (Req and MissingHeader), since you're not trying to diagnose why the module isn't available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, you can test this by adding a clang module whose module map contains e.g.:
module Foo {
requires missing
}
which should cause canImport(Foo) to fail. If you want a case where it succeeds, "requires swift" should do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
5721a19
to
399c029
Compare
The module loaders now have API to check whether a given module can be imported without importing the referenced module. This provides a significant speed boost to condition resolution and no longer introduces symbols from the referenced module into the current context without the user explicitly requesting it. The definition of ‘canImport’ does not necessarily mean that a full import without error is possible, merely that the path to the import is visible to the compiler and the module is loadable in some form or another. Note that this means this check is insufficient to guarantee that you are on one platform or another. For those kinds of checks, use ‘os(OSNAME)’.
@swift-ci please smoke test. |
LGTM |
⛵️ |
This reverts the contents of swiftlang#5778 and replaces it with a far simpler implementation of condition resolution along with canImport. When combined with the optimizations in swiftlang#6279 we get the best of both worlds with a performance win and a simpler implementation.
This reverts the contents of swiftlang#5778 and replaces it with a far simpler implementation of condition resolution along with canImport. When combined with the optimizations in swiftlang#6279 we get the best of both worlds with a performance win and a simpler implementation.
A bit of cleanup and a few optimizations around the recently-landed condition resolution stuff.