Skip to content

Conversation

@marek-safar
Copy link
Contributor

This change also

  • Adds simple inlining of method return values.
  • Fixes several shortcomings of existing implementation like ignoring
    methods called from unreachable blocks.

This helps to reduce the size of all runtime assemblies. For example size
of SPC for the default iOS project is reduced by 4% mostly due to metadata
removal. The linker speed improves a little bit (~3%) for the same
project.

@marek-safar marek-safar changed the title Add constant propagation through methods with parameter or complex bodies Add constant propagation through methods with parameters or complex bodies Jan 28, 2022
@MichalStrehovsky
Copy link
Member

I'm not actually reviewing this, but I had a couple areas of interest that I skimmed:

  • Inlining string literals across modules is observable; we avoid doing it in CoreCLR: https://github.com/dotnet/runtime/blob/017744628361a6fc45c05c75cf640292c670a8dd/src/coreclr/vm/jitinterface.cpp#L8082-L8090
  • Doing string concat ahead of time is observable too for similar reasons (if the newly concatenated string matches an existing literal). As the CoreCLR comment says "bugs are going to be tough to find". An example that has been brought up in the past is if someone takes a lock with such string, or uses object.ReferenceEqual because they expected a fresh string.
  • Is there anything to prevent removing calls to methods on types with static constructors/module constructors?
  • I think this is currently removing side effect for a callvirt with a null this (static void Call(Foo instance) => instance.X(); - I saw a check for ldarg_0, but I didn't see a check for whether the calling method is instance).

@marek-safar marek-safar marked this pull request as draft January 29, 2022 08:59
@marek-safar
Copy link
Contributor Author

marek-safar commented Jan 29, 2022

Inlining string literals across modules is observable; we avoid doing it in CoreCLR

Do you have an example why? I didn't understand the comment.

Doing string concat ahead of time is observable too for similar reasons

Ugh, this is quite corner case but fair enough will address that.

The code is not used for actually inlining only for result evaluation.

Is there anything to prevent removing calls to methods on types with static constructors/module constructors?

I had logic there but removed, now thinking about it again it needs more work even for other scenarios.

I think this is currently removing side effect for a callvirt with a null this

It does not but there is a bug in the logic (missing a special case for static context).

@MichalStrehovsky
Copy link
Member

Inlining string literals across modules is observable; we avoid doing it in CoreCLR

Do you have an example why? I didn't understand the comment.

It has to do with CompilationRelaxations.NoStringInterning (that the C# compiler automatically emits into all assemblies it produces). The docs on it are pretty poor, here's StackOverflow: https://stackoverflow.com/questions/15601916/how-does-compilationrelaxations-nostringinterning-actually-work. That said, I looked into the implementation on the runtime side and at this point (after summer of last year) it seems like we no longer need to worry about it.

I created dotnet/runtime#64521 to really double check it's no longer a concern. I expect JanK to scream if it's not the case and I'm wrong.


if (left is string sleft && right is string sright) {
if (method.Name.Length == 6)
return Instruction.Create (OpCodes.Ldstr, string.Concat (sleft, sright));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can create a very long string if called recursively.

Just curious - what are examples of CoreLib methods where this helps with trimming?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not matter if it creates long string as this logic is used for result computation only (value is not inlined). I found it for example here https://cs.github.com/dotnet/runtime/blob/f85ea976f81945ea18cd5dc71959cccecdc93cd2/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs?q=GetUtcFullDisplayName#L2111

…dies

This change also
- Adds simple inlining of method return values.
- Fixes several shortcomings of existing implementation like ignoring
methods called from unreachable blocks.

This helps to reduce the size of all runtime assemblies. For example size
of SPC for the default iOS project is reduced by 4% mostly due to metadata
removal. The linker speed improves a little bit (~3%) for the same
project.
@marek-safar
Copy link
Contributor Author

Is there anything to prevent removing calls to methods on types with static constructors/module constructors?

There is extra logic to take care of that.

I think this is currently removing side effect for a callvirt with a null this (static void Call(Foo instance) => instance.X(); - I saw a check for ldarg_0, but I didn't see a check for whether the calling method is instance).

Yep, made the check more comprehensive.

@marek-safar marek-safar marked this pull request as ready for review February 2, 2022 16:52
sizeOfImpl = (IntPtrSize ??= FindSizeMethod (_context.TryResolve (operand)));
}
var cpl = new CalleePayload (md, GetArgumentsOnStack (md, body.Instructions, i));
MethodResult? call_result = optimizer.TryGetMethodCallResult (cpl, optimizer.GetRecursionGuard ());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a major difference in how the implementation worked before - the previous version didn't have on-the-stack recursion, it handled this by storing info on the heap. I don't remember if we ever measured the maximum depth reached, but in the previous version it would not matter all. In this version the linker could run out of stack space and hard crash.

It took some non-trivial logic to make the code work well with on-the-heap storage, so the on-the-stack version is simpler. It could be OK this way, but we should be aware of it.

At the very least we should add a hard coded constant and cap the stack depth - basically if we run out of stack linker would not crash, but it would not optimize as well as it could. Much better than hard crashing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the deep stack check, the implementation goes only up to 3 iterations down on the default iOS app so I think this is fine.

}
}

processor.Replace (i, result.GetPrototype ());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must admit I find the name GetPrototype pretty confusing - it's a copy (or shallow copy), why not call it that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be both, shallow copy for complex instruction, deep copy for simple instruction. I have upstream PR which I'll merge here once approved (including the name)


[MemberNotNullWhen (true, "Result")]
public bool Analyze ()
public bool Analyze (in CalleePayload callee, Stack<MethodDefinition> callStack)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very similar to MethodBodyScanner - might be worth looking into potential sharing. Or at the very least comparing the two implementations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said - with the linker switching over to the "solver based data flow", the method body scanner will probably change quite a bit - especially around branching and such (basically it will become BasicBlockScanner instead).

Copy link
Member

@vitek-karas vitek-karas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks good now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants