Skip to content

Improve scalability in the JoinableTask dependency chain - #1018

Merged
Lifeng Lu (lifengl) merged 9 commits into
mainfrom
dev/lifengl/parentChainScale
Apr 14, 2022
Merged

Lifeng Lu (lifengl) merged 9 commits into
mainfrom
dev/lifengl/parentChainScale

Conversation

@lifengl

Copy link
Copy Markdown
Member

It was found during recently memory Watson bug investigation. When product code creates lots of dependent tasks of AsyncLazy, it can lead huge amounts of arrays to be allocated in the memory, although almost of them are released, they lead GC pressure or gets into LOH in this case.

Although this usage case has its own problem, it appears that the original data structure to hold child->parent relationship by using ListOfOftenOne was no longer suitable after many design changes in this area. It creates a new array when each item is added or removed was the primary reason behind this issue.

I replaced this with a new slightly different data structure, which tries to retain some advantage of the original data structure to be memory efficient when it holds a small amount of data (which is the majority of the time). However, the new data structure works more like List, so it retains a buffer larger than number of items it holds. I didn't switch to List, because the overhead of another layer of objects, and also List has extra data structure to prevent thread-safe/reentry problems. In our usage scenario, we don't need thread-safe as JTF is using it inside a sync lock, and reentry was prevented by not exposing any enumeration directly.

As our analysis and a previous change to JoinAsync, removing item from the list is a rare operation, so i leave it to O(n) (to find the item, then we fill the hole with the last item in the list). But adding item is mostly O(1), and enumeration will be done by resetting the original collection clear. It is used only when JTF is completed. I added ToArray, which is used by both unit tests and also a diagnostic code in JTF (not in common time).

…azy<T>.

The problem is that ListOfOftenOne was not designed for the current usage. When we add lots of items to it, it will repeat allocating arrays. A new data structure was created to retain some advantage of the original one like more efficient to hold 0 or 1 items. (And comparing to List<T>, it saves memory for small amount of items, as we do not need any thread-safe related design here.

We don't allow enumerating items directly so we don't need any reentry protection. It was completely designed to fit the current need. Consider we rarely remove a JoinableTask from a collection, and most JoinAsync will wait until the task to finish, it is rarely that we need remove any added item, so the design is to keep Add close to O(1), and remove O(n).
They were copied from tests for ListOfOftenOne to ensure new data structure to work in the similar way.
@AArnott

Copy link
Copy Markdown
Member

also List has extra data structure to prevent thread-safe/reentry problems

It does? I didn't think List<T> was thread-safe at all. And I wonder how List<T> could ever suffer from reentrancy given it never takes a delegate IIRC. That said, it does represent another object allocation beyond the array, and what you have is simple enough I think.

@AArnott Andrew Arnott (AArnott) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good overall.

@lifengl

Copy link
Copy Markdown
Member Author

also List has extra data structure to prevent thread-safe/reentry problems

It does? I didn't think List<T> was thread-safe at all. And I wonder how List<T> could ever suffer from reentrancy given it never takes a delegate IIRC. That said, it does represent another object allocation beyond the array, and what you have is simple enough I think.

no it doesn't, but it predefined fields like _syncRoot to make it easier without another wrapping, which is unfortunately a waste. It has another _version fields to detect thread-safe in the middle of enumeration (or reentry issue). Plus the extra object header.

@lifengl

Copy link
Copy Markdown
Member Author

not sure what leads the copyright head error. It appeared that nothing changed since the earlier build.

@AArnott Andrew Arnott (AArnott) changed the title Fix a scalability in the JoinableTask dependency chain Improve scalability in the JoinableTask dependency chain Apr 14, 2022
@AArnott Andrew Arnott (AArnott) added this to the v17.3 milestone Apr 14, 2022
@AArnott
Andrew Arnott (AArnott) force-pushed the dev/lifengl/parentChainScale branch from 681cb0d to 20075b6 Compare April 14, 2022 12:25
@AArnott

Copy link
Copy Markdown
Member

Lifeng Lu (@lifengl) The build break was because of an incompatibility with another change in main where stylecop rules changed the expected header in the file. You'd only see the break once your branch merged with main. I've made the necessary correction in your branch and pushed a few extra assertions to one of your new tests. It should be good to go now.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants