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

Bring back old array enumerator code #88371

Merged
merged 4 commits into from
Jul 7, 2023

Conversation

MichalStrehovsky
Copy link
Member

This is a 0.3% size saving for Stage1. We not only allow array enumerators to be preinitialized again, but also avoid introducing many array MethodTables (looks like the new enumerators force array MethodTable for cases where we could have avoided them).

I shaped the old code to look like after the refactor in #82751, but reintroduced the old classes.

Fixes #82993.

Cc @dotnet/ilc-contrib

This is a 0.3% size saving for Stage1. We not only allow array enumerators to be preinitialized again, but also avoid introducing many array `MethodTables` (looks like the new enumerators force array MethodTables for cases where we could have avoided them).

I shaped the old code to look like after the refactor in dotnet#82751, but reintroduced the old classes.

Fixes dotnet#82993.
@ghost
Copy link

ghost commented Jul 4, 2023

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas
See info in area-owners.md if you want to be subscribed.

Issue Details

This is a 0.3% size saving for Stage1. We not only allow array enumerators to be preinitialized again, but also avoid introducing many array MethodTables (looks like the new enumerators force array MethodTable for cases where we could have avoided them).

I shaped the old code to look like after the refactor in #82751, but reintroduced the old classes.

Fixes #82993.

Cc @dotnet/ilc-contrib

Author: MichalStrehovsky
Assignees: -
Labels:

area-NativeAOT-coreclr

Milestone: -

}
}

void IEnumerator.Reset()
Copy link
Member

@jkotas jkotas Jul 4, 2023

Choose a reason for hiding this comment

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

Should this be in the base class - same as in the shared one?

@jkotas
Copy link
Member

jkotas commented Jul 4, 2023

looks like the new enumerators force array MethodTable for cases where we could have avoided them

What are those cases? The array enumerator should be only reachable if we have created the array.

@jkotas
Copy link
Member

jkotas commented Jul 4, 2023

What would be the downside of using the native AOT enumerator everywhere? The only measurable downside that I can see is that the enumerator bbject would be 4 bytes larger on 32-bit platforms that we do not care about a whole lot.

{
if ((uint)_index >= (uint)_endIndex)
ThrowHelper.ThrowInvalidOperationException();
return _array[_index];
Copy link
Contributor

Choose a reason for hiding this comment

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

This will bounds check on every access, should this maybe use GetArrayDataReference and Unsafe.Add to avoid doing so?

Copy link
Member

Choose a reason for hiding this comment

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

Is this enumerator used often enough on hot paths for it to make a meaningful difference?

In general, we have stayed away from manual bounds-check elimination using unsafe code in collections.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this enumerator used often enough on hot paths for it to make a meaningful difference?

I'd say that arrays are fairly often passed to methods taking IEnumerable<T> and this would regress all such usages.

Copy link
Contributor

Choose a reason for hiding this comment

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

In general, we have stayed away from manual bounds-check elimination using unsafe code in collections.

Looks like we have an explicit bounds check on line 1244 now, so eliminating the one built into array accesses would be safe and worthwhile here.

protected int _index;
protected int _endIndex;

internal SZGenericArrayEnumeratorBase()
Copy link
Contributor

Choose a reason for hiding this comment

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

would it be better to capture the _endIndex in the construction as the whole point of this base class is to inline and read-only the the length?

}
}

object IEnumerator.Current
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this style change now preferred to the other way in line 143?

     object? IEnumerator.Current => Current;

@MichalStrehovsky
Copy link
Member Author

looks like the new enumerators force array MethodTable for cases where we could have avoided them

What are those cases? The array enumerator should be only reachable if we have created the array.

Generic dictionaries end up referring to the MethodTable in the diff I was looking at. I didn't investigate how exactly, but it's plausible in the case where the scanner ended up scanning more stuff than ended up needed.

What would be the downside of using the native AOT enumerator everywhere? The only measurable downside that I can see is that the enumerator bbject would be 4 bytes larger on 32-bit platforms that we do not care about a whole lot.

If we don't care about that, I can unify this. I only wanted to restore the old code to avoid sidetracking this PR into any sort of runtime perf conversation - this is restoring code that Native AOT shipped with on .NET 7 so it doesn't do anything with runtime perf there. If we do this for all runtimes, this suddenly becomes a runtime perf conversation. The point of this PR is just to fix the size regression.

@jkotas
Copy link
Member

jkotas commented Jul 5, 2023

Could you please try to do this for all runtimes without ifdef?

Copy link
Contributor

@IDisposable IDisposable left a comment

Choose a reason for hiding this comment

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

Nice

_index = -1;
_endIndex = endIndex;
}

public bool MoveNext()
Copy link
Member

Choose a reason for hiding this comment

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

        public bool MoveNext()
        {
            int index = _index + 1;
            if ((uint)index < (uint)_endIndex)
            {
                _index = index;
                return true;
            }
            _index = _endIndex;
            return false;
        }

This should be better, on some architectures at least. (One less comparison.)

@jkotas
Copy link
Member

jkotas commented Jul 6, 2023

I have run a quick microbenchmark

int Sum(IEnumerable<int> a)
{
    int sum = 0;
    foreach (var i in a) sum += i;
    return sum;
}

called on int[1000]

Surprisingly, this implementation is faster on this microbenchmark on my machine.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

Thanks

MichalStrehovsky and others added 2 commits July 7, 2023 12:31
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
@MichalStrehovsky
Copy link
Member Author

Surprisingly, this implementation is faster on this microbenchmark on my machine.

Nice, thanks for measuring! That's an unexpected bonus.

The failures are all #88499, merging.

@MichalStrehovsky MichalStrehovsky merged commit 4285e43 into dotnet:main Jul 7, 2023
160 of 167 checks passed
@MichalStrehovsky MichalStrehovsky deleted the fix82993 branch July 7, 2023 07:38
@runfoapp runfoapp bot mentioned this pull request Jul 7, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Aug 6, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Array enumerators are not preinitialized anymore
4 participants