Skip to content
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

Crossgen2 determinism fixes #164

Closed
wants to merge 2 commits into from
Closed

Crossgen2 determinism fixes #164

wants to merge 2 commits into from

Conversation

SrivastavaAnubhav
Copy link
Contributor

  • NativeFormatWriter's HashTable save now uses OrderBy, so the output deterministic if the values are entered deterministically
  • Lock GetILBytes in EcmaMethodIL so we don't accidentally get two different addresses for the same code
  • Revert ManifestMetadataTableNode change to an older version: we don't add the input module's assemblies to manifestAssemblies in the constructor (we add to assemblyRefToIdMap) so some things were failing. The dictionary might also be faster than the List for lookup.
  • Do not flush the ILCache before compiling all methods in a batch. This might have caused the same MethodDesc to have two different MethodIL objects.

@SrivastavaAnubhav
Copy link
Contributor Author

@dotnet/crossgen-contrib

return (_ilBytes = ilBytes);
// This lock is necessary since the JIT requires that we provide the same address
// for the IL buffer each time (it uses the address to detect recursive calls).
// Since MethodBodyBlock.GetAllBytes creates a new array each time, if two threads
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Just mention the name of the API we're calling here (GetILBytes) to reduce confusion. There's no GetAllBytes here.

Copy link
Member

Choose a reason for hiding this comment

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

Consider using Lazy to avoid writing our own double-checked locking?

Copy link
Contributor

@fadimounir fadimounir Nov 21, 2019

Choose a reason for hiding this comment

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

I like what Jan suggested here, to use Interlocked.CompareExchange


if (!_manifestAssemblies.Contains(assemblyName))
int assemblyRefIndex;
if (!_assemblyRefToModuleIdMap.TryGetValue(assemblyName.Name, out assemblyRefIndex))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is wrong: this will cause missing entries in the _manifestAssemblies (the bug that I fixed recently).

The right way to fix this is to get rid of _manifestAssemblies, and emit a sorted version of _assemblyRefToModuleIdMap in GetData(), sorted by module ID

Copy link
Contributor

Choose a reason for hiding this comment

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

And make _assemblyRefToModuleIdMap a Dictionary<AssemblyName, int>

EcmaModule targetModule = _signatureContext.GetTargetModule(_arrayType);
SignatureContext innerContext = dataBuilder.EmitFixup(r2rFactory, ReadyToRunFixupKind.READYTORUN_FIXUP_NewArray, targetModule, _signatureContext);
dataBuilder.EmitTypeSignature(_arrayType, innerContext);
if (!relocsOnly)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: would be nice to follow the same convention we have in other places:

            if (relocsOnly)
            {
                return new ObjectData(Array.Empty<byte>(), null, 1, null);
            }

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 looks like we use a mix of both, but I like the if (relocsOnly) one better because of the indentation. Can I move all of them over to that one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mean the ones which return trivially like that. There are some legitimate uses of it as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

In that case, leave it the way it is.

Copy link
Contributor

@fadimounir fadimounir left a comment

Choose a reason for hiding this comment

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

Generally looks good, but needs a few fixes.

As @davidwrighton mentioned, you will need to think about how to add tests for this determinism work. That's the kind of stuff that is very easy for other folks to break if there's no test to catch it.

@fadimounir
Copy link
Contributor

if there's no test to catch it.

A good place to start is to add a test that compiles all managed assemblies in coreroot twice with different seeds, and diffs the output.

if (_methodILCache.Count > 1000)
_methodILCache = new ILCache(_methodILCache.ILProvider, NodeFactory.CompilationModuleGroup);

// TODO: Remove the ILCache and use the pinned heap for the IL stream once this GC
Copy link
Member

Choose a reason for hiding this comment

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

I do not see why we would ever want to remove the ILCache. Using pinned heap is not going to help. I would delete this comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

why we would ever want to remove the ILCache

I don't see why we need it in the first place. This just seems to help releasing the pinning of the IL byte arrays to reduce GC fragmentation. The IL streams are saved on the type system method object, and there's no point in "caching".

My understanding is that the pinned heap will not have fragmentation. If so, we should just allocate the IL byte arrays on it, and remove the ILCache

Copy link
Member

Choose a reason for hiding this comment

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

ILCache does not do any pinning. All pinning is done by jitinterface (CorInfoImpl.cs).

The purpose of ILCache is to cache the ILCode for given MethodDesc. The algorithm to compute ILCode for given MethodDesc is not cheap.

My understanding is that the pinned heap will not have fragmentation.

Pinned heap will have tend to have worse fragmentation than regular heaps because of it cannot be compacted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, I see what you mean. I debugged this, and it's more involved than I thought. I was under the impression that GetMethodIL() would directly just go to the EcmaMethodDesc's IL byte array that we load from metadata. This is why I was under the impression that the ILCache was used to free up the pins.

@SrivastavaAnubhav please delete the comment then.

// call this on the same function then it is possible for both to set _ilBytes.
// Then, one of the threads will receive the first value on the first call and the
// second value on all subsequent calls, and we will not detect the recursive call.
lock (this)
Copy link
Member

Choose a reason for hiding this comment

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

This should rather use Interlocked.CompareExchange

…areExchange. Remove comment saying that ILCache would eventually be removed. Fix ModuleToIndex not adding all assemblies.
byte[] ilBytes = _methodBody.GetILBytes();
return (_ilBytes = ilBytes);
}
Interlocked.CompareExchange(ref _ilBytes, _methodBody.GetILBytes(), null);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this needs to be in a loop to handle race conditions

Copy link
Contributor

Choose a reason for hiding this comment

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

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 correct as is. No loop needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, right. Sorry, my bad. I got confused by the so many other compare-exchanges we have in the runtime code base :). Thanks for catching that Jan!

@@ -163,7 +176,8 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
fieldList: MetadataTokens.FieldDefinitionHandle(1),
methodList: MetadataTokens.MethodDefinitionHandle(1));

foreach (AssemblyName assemblyName in _manifestAssemblies)
IEnumerable<AssemblyName> manifestAssemblies = _moduleIdToAssemblyNameMap.OrderBy(x => x.Key).Select(x => x.Value);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: It might look nicer to write it without .Select(), like:

foreach (var index in _moduleIdToAssemblyNameMap.Keys.OrderBy(...))
{
    use _moduleIdToAssemblyNameMap[index]
}

@fadimounir
Copy link
Contributor

The changes look good to me and ready to merge (modulo the merge conflicts). The only remaining thing so far is adding some tests to catch regressions.

@SrivastavaAnubhav
Copy link
Contributor Author

My fork was messed up by the private -> public switch of this repo, so I've remade the fork and the PR here: #306

@ghost ghost locked as resolved and limited conversation to collaborators Dec 11, 2020
@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Mar 9, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-crossgen2-coreclr in-pr There is an active PR which will close this issue when it is merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants