[CI ONLY] [NativeAOT] ObjWriter in C# - #92705

Closed
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3
Closed

[CI ONLY] [NativeAOT] ObjWriter in C##92705
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3

Conversation

@filipnavara

@filipnavarafilipnavara commented Sep 27, 2023

Copy link
Copy Markdown
Member

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Tracking list of issues found by the CI:

  • linux-x64: System.NotSupportedException: Unsupported relocation: IMAGE_REL_TLSGD
  • linux-arm64: relocation R_AARCH64_TLSDESC_ADR_PAGE21 cannot be used against symbol 'tls_InlinedThreadStatics'
  • linux-arm64: libunwind: malformed DW_CFA_register DWARF unwind, reg too big
  • linux-arm64: Different pointer encoding is used in EH frames which seems to cause issues
  • iOS/tvOS: warning: ignoring file /tmp/helix/working/B5450932/w/C6420AD0/e/publish/native/iOS.Device.Aot.Test.o, building for iOS-arm64 but attempting to link with file built for unknown-unsupported file format ( 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 )
  • win-x64: Framework.lib(System.Private.CoreLib.obj) : fatal error LNK1243: invalid or corrupt file: COMDAT section 0x20001 associated with following section 0x0 (MultiModule test)
  • win-arm64: CoreFXTestLibrary.AssertTestException: Assert.AreEqual: Expected: [System.Func2[CommonType10[],CommonType10[]]]. Actual: [System.Func2[CommonType10[],CommonType10[]]].

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.Reflection.Metadata labels Sep 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

area-System.Reflection.Metadata, community-contribution

Milestone:-

@ghost

Copy link
Copy Markdown

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

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

community-contribution, area-NativeAOT-coreclr

Milestone:-

@filipnavarafilipnavara added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it labels Sep 27, 2023
@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

@am11

am11 commented Sep 28, 2023

Copy link
Copy Markdown
Member

This one looks a bit less mysterious:

Running Test: ExistingInstantiations.Test.TestWithExistingInst
Caught Unexpected exception:System.NotSupportedException: Cannot retrieve a MethodInfo for this delegate because the method it targeted was not enabled for metadata.
at Internal.Reflection.Extensions.NonPortable.DelegateMethodInfoRetriever.GetDelegateMethodInfo(Delegate) + 0x29c
at ExistingInstantiations.Test.TestWithExistingInst() + 0x518
at CoreFXTestLibrary.Internal.Runner.RunTestMethod(TestInfo) + 0x364
at CoreFXTestLibrary.Internal.Runner.RunTest(TestInfo) + 0x20
---- Test FAILED ---------------

perhaps TestReverseLookupsWithArrayArg failure has the same underlying reason?

BTW, CI is using precisely this toolchain version:

 -- The C compiler identification is MSVC 19.36.32537.0
-- The CXX compiler identification is MSVC 19.36.32537.0

to cross-compile for arm64 on x64: build.cmd -ci -arch arm64 -os windows -s clr.aot+host.native+libs+tools.illink -c Release -rc Release -lc Release -hc Release

Comment threadsrc/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj Outdated
@filipnavara

Copy link
Copy Markdown
MemberAuthor

This one looks a bit less mysterious:

Thanks, @am11. I will check that one. I will likely wait till tomorrow to have direct access to the Win/ARM machine instead of just RDP-over-RDP-over-Tailscale. I have an uncommitted code where I match more closely the COFF output from the old ObjWriter so it's diffable to certain extent, and easier to spot differences. The DynamicGenerics test produces high number of sections and relocations and triggers the "big obj" code paths which didn't get much testing, so that's my primary suspicion.

@TIHan

Copy link
Copy Markdown
Contributor

Thank you for making the PR @filipnavara .

I'm out for a bit and hopefully will look at this next week. So far it looks promising.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

Turns out the error was somewhere between the chair and the keyboard. I had a wrong branch checked out.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

I found the issue in the COFF/ARM64 code. I was incorrectly ignoring the addend for IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A (and it was actually an Assert on debug build). This broke fat pointers that use the +2 address offset to be distinguished from real pointers. The same issue didn't happen for Mach-O since it rewrites this particular relocation into a more complex one. On ELF we emit the RELA section with addend inside the relocations, not inside the code, so it happened to work. COFF is the only platform that embeds this kind of relocation in the data.

While searching for the root cause I found couple more issues that are mostly harmless but should be fixed nevertheless. I'll clean up the code and commit it soon.

Comment threadsrc/coreclr/tools/aot/external/LibObjectFile/changelog.md Outdated
@filipnavara

filipnavara commented Sep 30, 2023

Copy link
Copy Markdown
MemberAuthor

I think the code now reached a point where it passes the smoke tests for the supported platforms, which was the primary purpose of this PR. I'll keep it open for the moment, but it served its purpose.

Aside from some general structural improvements these are the areas I intend to explore next (in no particular order):

  • Emitting DWARF debugging info eagerly and directly into corresponding sections without the intermediate representation. (partially done, on par with LLVM ObjWriter)
  • Resolve relative relocations within same section before writing them down to the object files. This should have no effect on output after linking. It does, however, produce slightly smaller object files, and it may avoid hitting limits on the number of relocations in certain formats.
  • Optimize the string table building to take advantage of suffix matches. All the formats use some variation of string table with null-terminated string referenced by offset. It's possible to generate smaller object files by producing _unwind0_XYZ at offset N, and then reusing the suffix XYZ at offset N+9 as a different string.
  • Explore generating XDATA/PDATA unwinding info in COFF to be closer to the structure that MSVC produces. (done)
  • Implement COMDAT handling for ELF file format. It's the one big missing thing for feature parity. (done)

@am11

am11 commented Oct 1, 2023

Copy link
Copy Markdown
Member

According to dotnet-trace on linux-arm64. ilc.dll spends > 11 seconds in

libobjectfile!LibObjectFile.Elf.ElfWriter`1[LibObjectFile.Elf.ElfEncoderDirect].WriteSections()

during System.Runtime.Tests publishing (and output object size is 273M vs. main's 244M). With published-ilc, this test OOMs (code 137, the current CI failure).

speedscope-trace.zip

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@am11 Thanks for looking into it, really appreciated!

I didn't focus on the performance and memory usage outside of isolated scenarios. I profiled some code paths for COFF and CodeView but there's very little overlap with what ELF and DWARF does. I'll check the trace you provided and do some profiling on my side as well.

The output size difference is expected. It's mostly caused by extra relocations in the output and non-optimized string table (as mentioned in the "future work" list above). There's also some difference in debugging info size but not nearly as big.

Notably, the memory usage of the DWARF debugging info emitter is pretty high. It's not easy to refactor without significant changes to LibObjectFile. It operates on a "document" model (akin to JsonDocument but binary and multiple cross-linked documents), while we really need something closer to the "writer" model (akin to Utf8JsonWriter). There's already an internal model present in the compiler and it is memory intensive to convert from one model to the other. Additionally, the DWARF model in LibObjectFile has additional abstraction for relocations which are then converted to ELF relocations. This all adds up both in memory and time profiles.

I did some comparisons with the debugging info turned off, and the results were largely favorable to the C# implementation. That said, I did it primarily on Mach-O, not ELF.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

speedscope-trace.zip

Wow, I totally didn't expect ElfObjectFile.Verify to take 5 times as much as ElfObjectFile.WriteSections. I knew ElfStringTable.GetOrCreateIndex is extremely inefficient but this exceeded all my expectations. It tries to do suffix matching but in the process creates a ton of strings. Given all the time spent in the method I suspect the memory usage culprit could be there as well.

@filipnavara

filipnavara commented Oct 2, 2023

Copy link
Copy Markdown
MemberAuthor

Since some people are apparently following the PR and looking at some of the performance issues, I made an isolated sample showing the problem with ElfStringTable: https://github.com/filipnavara/StringTableBenchmark

I used three different algorithms to build the string table:

  • Naive produces string table with no suffix deduplication (similar to the code used for COFF)
  • MultiKeySort sorts the input strings and then produces optimized string table with deduplicated suffixes (similar to the LLVM string table builder)
  • ElfStringTable uses LibObjectFile's ElfStringTable, which incrementally builds the table and uses dictionary to deduplicate suffixes

Note that ElfStringTable technically provides an incremental API where the full set of string is not known beforehand. However, in reality it's pre-populated with the full string set anyway, and this property can be used for optimizing the common case without changing the external API.

Without further ado, here are the results on my MacBook Air M1 for a sample string set taken from UseSystemResourceKeys.o in the smoke tests. The set contains slightly less than 25k strings where many can be suffix compressed.

MethodMeanErrorStdDevGen0Gen1Gen2Allocated
ElfStringTable350.838 ms6.8811 ms8.1914 ms38000.000021000.00007000.0000316.81 MB
MultiKeySort11.456 ms0.0469 ms0.0439 ms1062.50001046.8750968.75004.87 MB
Naive2.374 ms0.0060 ms0.0050 ms1042.96881015.6250996.09385.82 MB

You can clearly see that the approach used by ElfStringTable doesn't scale well.

--

The input is pre-sorted, so the 3-way radix quick sort in MultiKeySort happens to choose unoptimal pivot, which skews the result a bit. I didn't spend much time micro-optimizing it, so it serves more as a ballpark figure.

--

I updated the MultiKeySort version a bit to get 30% speed improvement. The size of the output is 2,457,077 bytes for the Naive implementation, and 1,896,752 for the MultiKeySort one.

@filipnavara
filipnavaraforce-pushed the objwriter3 branch 2 times, most recently from 00bfd74 to 1c0d928CompareOctober 2, 2023 16:06
@am11

am11 commented Oct 2, 2023

Copy link
Copy Markdown
Member

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

@agocke

Copy link
Copy Markdown
Member

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

I really appreciate that you checked and helped diagnose this issue. 👍 I am really happy to have some working baseline version of the changes before I proceed to do further optimizations and experiments.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

I committed an experimental fix and it passed the CI. I'll submit it upstream and then focus on the further work I mentioned above.

@TIHan

TIHan commented Oct 2, 2023

Copy link
Copy Markdown
Contributor

@filipnavara , this is looking really good and glad others were able to look at it. The performance improvements do look great.

How far do you think your solution is from matching the existing functionality? It looks like the smoke tests are passing on all the platforms. @agocke , are there any scenarios that we need to cover that are not covered by the tests?

- Section names need to come first in the string table because of limited space for their reference by offset. This caused the "managedcode$I" and "modules$I" section names to be garbage when there were many symbols.
- Fix missing array pool return.
Remove LibObjectFile dependency
foreach (var sequencePoint in sequencePoints)
{
if (lastFileName == null || lastFileName != sequencePoint.FileName)
if (lastFileName is null || lastFileName != sequencePoint.FileName)

@am11am11Dec 11, 2023

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.

If you prefer pattern matching terse syntax, we can reduce the verbosity a bit in those long RelocType.XX conditions:

usingstaticILCompiler.DependencyAnalysis.RelocType;
...if(relocTypeisIMAGE_REL_BASED_ARM64_BRANCH26 or IMAGE_REL_BASED_ARM64_PAGEBASE_REL21 or
IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A or IMAGE_REL_AARCH64_TLSLE_ADD_TPREL_HI12 or ..)
...

(the duplication due to IMAGE_REL_ prefix is enough as-is 😅)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I used several different code styles as I progressed, sometimes intentionally, sometimes as a consequence of reusing existing code... I am generally open to ideas how to keep the code as terse and readable as possible ;-)

@am11am11 mentioned this pull request Jan 8, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jan 11, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrcommunity-contributionIndicates that the PR has been added by a community memberNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)NO-REVIEWExperimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@filipnavara@am11@TIHan@agocke@SingleAccretion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[CI ONLY] [NativeAOT] ObjWriter in C# - #92705

Closed
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3
Closed

[CI ONLY] [NativeAOT] ObjWriter in C##92705
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3

Conversation

@filipnavara

@filipnavarafilipnavara commented Sep 27, 2023

Copy link
Copy Markdown
Member

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Tracking list of issues found by the CI:

  • linux-x64: System.NotSupportedException: Unsupported relocation: IMAGE_REL_TLSGD
  • linux-arm64: relocation R_AARCH64_TLSDESC_ADR_PAGE21 cannot be used against symbol 'tls_InlinedThreadStatics'
  • linux-arm64: libunwind: malformed DW_CFA_register DWARF unwind, reg too big
  • linux-arm64: Different pointer encoding is used in EH frames which seems to cause issues
  • iOS/tvOS: warning: ignoring file /tmp/helix/working/B5450932/w/C6420AD0/e/publish/native/iOS.Device.Aot.Test.o, building for iOS-arm64 but attempting to link with file built for unknown-unsupported file format ( 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 )
  • win-x64: Framework.lib(System.Private.CoreLib.obj) : fatal error LNK1243: invalid or corrupt file: COMDAT section 0x20001 associated with following section 0x0 (MultiModule test)
  • win-arm64: CoreFXTestLibrary.AssertTestException: Assert.AreEqual: Expected: [System.Func2[CommonType10[],CommonType10[]]]. Actual: [System.Func2[CommonType10[],CommonType10[]]].

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.Reflection.Metadata labels Sep 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

area-System.Reflection.Metadata, community-contribution

Milestone:-

@ghost

Copy link
Copy Markdown

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

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

community-contribution, area-NativeAOT-coreclr

Milestone:-

@filipnavarafilipnavara added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it labels Sep 27, 2023
@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

@am11

am11 commented Sep 28, 2023

Copy link
Copy Markdown
Member

This one looks a bit less mysterious:

Running Test: ExistingInstantiations.Test.TestWithExistingInst
Caught Unexpected exception:System.NotSupportedException: Cannot retrieve a MethodInfo for this delegate because the method it targeted was not enabled for metadata.
at Internal.Reflection.Extensions.NonPortable.DelegateMethodInfoRetriever.GetDelegateMethodInfo(Delegate) + 0x29c
at ExistingInstantiations.Test.TestWithExistingInst() + 0x518
at CoreFXTestLibrary.Internal.Runner.RunTestMethod(TestInfo) + 0x364
at CoreFXTestLibrary.Internal.Runner.RunTest(TestInfo) + 0x20
---- Test FAILED ---------------

perhaps TestReverseLookupsWithArrayArg failure has the same underlying reason?

BTW, CI is using precisely this toolchain version:

 -- The C compiler identification is MSVC 19.36.32537.0
-- The CXX compiler identification is MSVC 19.36.32537.0

to cross-compile for arm64 on x64: build.cmd -ci -arch arm64 -os windows -s clr.aot+host.native+libs+tools.illink -c Release -rc Release -lc Release -hc Release

Comment threadsrc/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj Outdated
@filipnavara

Copy link
Copy Markdown
MemberAuthor

This one looks a bit less mysterious:

Thanks, @am11. I will check that one. I will likely wait till tomorrow to have direct access to the Win/ARM machine instead of just RDP-over-RDP-over-Tailscale. I have an uncommitted code where I match more closely the COFF output from the old ObjWriter so it's diffable to certain extent, and easier to spot differences. The DynamicGenerics test produces high number of sections and relocations and triggers the "big obj" code paths which didn't get much testing, so that's my primary suspicion.

@TIHan

Copy link
Copy Markdown
Contributor

Thank you for making the PR @filipnavara .

I'm out for a bit and hopefully will look at this next week. So far it looks promising.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

Turns out the error was somewhere between the chair and the keyboard. I had a wrong branch checked out.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

I found the issue in the COFF/ARM64 code. I was incorrectly ignoring the addend for IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A (and it was actually an Assert on debug build). This broke fat pointers that use the +2 address offset to be distinguished from real pointers. The same issue didn't happen for Mach-O since it rewrites this particular relocation into a more complex one. On ELF we emit the RELA section with addend inside the relocations, not inside the code, so it happened to work. COFF is the only platform that embeds this kind of relocation in the data.

While searching for the root cause I found couple more issues that are mostly harmless but should be fixed nevertheless. I'll clean up the code and commit it soon.

Comment threadsrc/coreclr/tools/aot/external/LibObjectFile/changelog.md Outdated
@filipnavara

filipnavara commented Sep 30, 2023

Copy link
Copy Markdown
MemberAuthor

I think the code now reached a point where it passes the smoke tests for the supported platforms, which was the primary purpose of this PR. I'll keep it open for the moment, but it served its purpose.

Aside from some general structural improvements these are the areas I intend to explore next (in no particular order):

  • Emitting DWARF debugging info eagerly and directly into corresponding sections without the intermediate representation. (partially done, on par with LLVM ObjWriter)
  • Resolve relative relocations within same section before writing them down to the object files. This should have no effect on output after linking. It does, however, produce slightly smaller object files, and it may avoid hitting limits on the number of relocations in certain formats.
  • Optimize the string table building to take advantage of suffix matches. All the formats use some variation of string table with null-terminated string referenced by offset. It's possible to generate smaller object files by producing _unwind0_XYZ at offset N, and then reusing the suffix XYZ at offset N+9 as a different string.
  • Explore generating XDATA/PDATA unwinding info in COFF to be closer to the structure that MSVC produces. (done)
  • Implement COMDAT handling for ELF file format. It's the one big missing thing for feature parity. (done)

@am11

am11 commented Oct 1, 2023

Copy link
Copy Markdown
Member

According to dotnet-trace on linux-arm64. ilc.dll spends > 11 seconds in

libobjectfile!LibObjectFile.Elf.ElfWriter`1[LibObjectFile.Elf.ElfEncoderDirect].WriteSections()

during System.Runtime.Tests publishing (and output object size is 273M vs. main's 244M). With published-ilc, this test OOMs (code 137, the current CI failure).

speedscope-trace.zip

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@am11 Thanks for looking into it, really appreciated!

I didn't focus on the performance and memory usage outside of isolated scenarios. I profiled some code paths for COFF and CodeView but there's very little overlap with what ELF and DWARF does. I'll check the trace you provided and do some profiling on my side as well.

The output size difference is expected. It's mostly caused by extra relocations in the output and non-optimized string table (as mentioned in the "future work" list above). There's also some difference in debugging info size but not nearly as big.

Notably, the memory usage of the DWARF debugging info emitter is pretty high. It's not easy to refactor without significant changes to LibObjectFile. It operates on a "document" model (akin to JsonDocument but binary and multiple cross-linked documents), while we really need something closer to the "writer" model (akin to Utf8JsonWriter). There's already an internal model present in the compiler and it is memory intensive to convert from one model to the other. Additionally, the DWARF model in LibObjectFile has additional abstraction for relocations which are then converted to ELF relocations. This all adds up both in memory and time profiles.

I did some comparisons with the debugging info turned off, and the results were largely favorable to the C# implementation. That said, I did it primarily on Mach-O, not ELF.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

speedscope-trace.zip

Wow, I totally didn't expect ElfObjectFile.Verify to take 5 times as much as ElfObjectFile.WriteSections. I knew ElfStringTable.GetOrCreateIndex is extremely inefficient but this exceeded all my expectations. It tries to do suffix matching but in the process creates a ton of strings. Given all the time spent in the method I suspect the memory usage culprit could be there as well.

@filipnavara

filipnavara commented Oct 2, 2023

Copy link
Copy Markdown
MemberAuthor

Since some people are apparently following the PR and looking at some of the performance issues, I made an isolated sample showing the problem with ElfStringTable: https://github.com/filipnavara/StringTableBenchmark

I used three different algorithms to build the string table:

  • Naive produces string table with no suffix deduplication (similar to the code used for COFF)
  • MultiKeySort sorts the input strings and then produces optimized string table with deduplicated suffixes (similar to the LLVM string table builder)
  • ElfStringTable uses LibObjectFile's ElfStringTable, which incrementally builds the table and uses dictionary to deduplicate suffixes

Note that ElfStringTable technically provides an incremental API where the full set of string is not known beforehand. However, in reality it's pre-populated with the full string set anyway, and this property can be used for optimizing the common case without changing the external API.

Without further ado, here are the results on my MacBook Air M1 for a sample string set taken from UseSystemResourceKeys.o in the smoke tests. The set contains slightly less than 25k strings where many can be suffix compressed.

MethodMeanErrorStdDevGen0Gen1Gen2Allocated
ElfStringTable350.838 ms6.8811 ms8.1914 ms38000.000021000.00007000.0000316.81 MB
MultiKeySort11.456 ms0.0469 ms0.0439 ms1062.50001046.8750968.75004.87 MB
Naive2.374 ms0.0060 ms0.0050 ms1042.96881015.6250996.09385.82 MB

You can clearly see that the approach used by ElfStringTable doesn't scale well.

--

The input is pre-sorted, so the 3-way radix quick sort in MultiKeySort happens to choose unoptimal pivot, which skews the result a bit. I didn't spend much time micro-optimizing it, so it serves more as a ballpark figure.

--

I updated the MultiKeySort version a bit to get 30% speed improvement. The size of the output is 2,457,077 bytes for the Naive implementation, and 1,896,752 for the MultiKeySort one.

@filipnavara
filipnavaraforce-pushed the objwriter3 branch 2 times, most recently from 00bfd74 to 1c0d928CompareOctober 2, 2023 16:06
@am11

am11 commented Oct 2, 2023

Copy link
Copy Markdown
Member

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

@agocke

Copy link
Copy Markdown
Member

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

I really appreciate that you checked and helped diagnose this issue. 👍 I am really happy to have some working baseline version of the changes before I proceed to do further optimizations and experiments.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

I committed an experimental fix and it passed the CI. I'll submit it upstream and then focus on the further work I mentioned above.

@TIHan

TIHan commented Oct 2, 2023

Copy link
Copy Markdown
Contributor

@filipnavara , this is looking really good and glad others were able to look at it. The performance improvements do look great.

How far do you think your solution is from matching the existing functionality? It looks like the smoke tests are passing on all the platforms. @agocke , are there any scenarios that we need to cover that are not covered by the tests?

- Section names need to come first in the string table because of limited space for their reference by offset. This caused the "managedcode$I" and "modules$I" section names to be garbage when there were many symbols.
- Fix missing array pool return.
Remove LibObjectFile dependency
foreach (var sequencePoint in sequencePoints)
{
if (lastFileName == null || lastFileName != sequencePoint.FileName)
if (lastFileName is null || lastFileName != sequencePoint.FileName)

@am11am11Dec 11, 2023

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.

If you prefer pattern matching terse syntax, we can reduce the verbosity a bit in those long RelocType.XX conditions:

usingstaticILCompiler.DependencyAnalysis.RelocType;
...if(relocTypeisIMAGE_REL_BASED_ARM64_BRANCH26 or IMAGE_REL_BASED_ARM64_PAGEBASE_REL21 or
IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A or IMAGE_REL_AARCH64_TLSLE_ADD_TPREL_HI12 or ..)
...

(the duplication due to IMAGE_REL_ prefix is enough as-is 😅)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I used several different code styles as I progressed, sometimes intentionally, sometimes as a consequence of reusing existing code... I am generally open to ideas how to keep the code as terse and readable as possible ;-)

@am11am11 mentioned this pull request Jan 8, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jan 11, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrcommunity-contributionIndicates that the PR has been added by a community memberNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)NO-REVIEWExperimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@filipnavara@am11@TIHan@agocke@SingleAccretion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[CI ONLY] [NativeAOT] ObjWriter in C# - #92705

Closed
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3
Closed

[CI ONLY] [NativeAOT] ObjWriter in C##92705
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3

Conversation

@filipnavara

@filipnavarafilipnavara commented Sep 27, 2023

Copy link
Copy Markdown
Member

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Tracking list of issues found by the CI:

  • linux-x64: System.NotSupportedException: Unsupported relocation: IMAGE_REL_TLSGD
  • linux-arm64: relocation R_AARCH64_TLSDESC_ADR_PAGE21 cannot be used against symbol 'tls_InlinedThreadStatics'
  • linux-arm64: libunwind: malformed DW_CFA_register DWARF unwind, reg too big
  • linux-arm64: Different pointer encoding is used in EH frames which seems to cause issues
  • iOS/tvOS: warning: ignoring file /tmp/helix/working/B5450932/w/C6420AD0/e/publish/native/iOS.Device.Aot.Test.o, building for iOS-arm64 but attempting to link with file built for unknown-unsupported file format ( 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 )
  • win-x64: Framework.lib(System.Private.CoreLib.obj) : fatal error LNK1243: invalid or corrupt file: COMDAT section 0x20001 associated with following section 0x0 (MultiModule test)
  • win-arm64: CoreFXTestLibrary.AssertTestException: Assert.AreEqual: Expected: [System.Func2[CommonType10[],CommonType10[]]]. Actual: [System.Func2[CommonType10[],CommonType10[]]].

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.Reflection.Metadata labels Sep 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

area-System.Reflection.Metadata, community-contribution

Milestone:-

@ghost

Copy link
Copy Markdown

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

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

community-contribution, area-NativeAOT-coreclr

Milestone:-

@filipnavarafilipnavara added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it labels Sep 27, 2023
@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

@am11

am11 commented Sep 28, 2023

Copy link
Copy Markdown
Member

This one looks a bit less mysterious:

Running Test: ExistingInstantiations.Test.TestWithExistingInst
Caught Unexpected exception:System.NotSupportedException: Cannot retrieve a MethodInfo for this delegate because the method it targeted was not enabled for metadata.
at Internal.Reflection.Extensions.NonPortable.DelegateMethodInfoRetriever.GetDelegateMethodInfo(Delegate) + 0x29c
at ExistingInstantiations.Test.TestWithExistingInst() + 0x518
at CoreFXTestLibrary.Internal.Runner.RunTestMethod(TestInfo) + 0x364
at CoreFXTestLibrary.Internal.Runner.RunTest(TestInfo) + 0x20
---- Test FAILED ---------------

perhaps TestReverseLookupsWithArrayArg failure has the same underlying reason?

BTW, CI is using precisely this toolchain version:

 -- The C compiler identification is MSVC 19.36.32537.0
-- The CXX compiler identification is MSVC 19.36.32537.0

to cross-compile for arm64 on x64: build.cmd -ci -arch arm64 -os windows -s clr.aot+host.native+libs+tools.illink -c Release -rc Release -lc Release -hc Release

Comment threadsrc/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj Outdated
@filipnavara

Copy link
Copy Markdown
MemberAuthor

This one looks a bit less mysterious:

Thanks, @am11. I will check that one. I will likely wait till tomorrow to have direct access to the Win/ARM machine instead of just RDP-over-RDP-over-Tailscale. I have an uncommitted code where I match more closely the COFF output from the old ObjWriter so it's diffable to certain extent, and easier to spot differences. The DynamicGenerics test produces high number of sections and relocations and triggers the "big obj" code paths which didn't get much testing, so that's my primary suspicion.

@TIHan

Copy link
Copy Markdown
Contributor

Thank you for making the PR @filipnavara .

I'm out for a bit and hopefully will look at this next week. So far it looks promising.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

Turns out the error was somewhere between the chair and the keyboard. I had a wrong branch checked out.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

I found the issue in the COFF/ARM64 code. I was incorrectly ignoring the addend for IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A (and it was actually an Assert on debug build). This broke fat pointers that use the +2 address offset to be distinguished from real pointers. The same issue didn't happen for Mach-O since it rewrites this particular relocation into a more complex one. On ELF we emit the RELA section with addend inside the relocations, not inside the code, so it happened to work. COFF is the only platform that embeds this kind of relocation in the data.

While searching for the root cause I found couple more issues that are mostly harmless but should be fixed nevertheless. I'll clean up the code and commit it soon.

Comment threadsrc/coreclr/tools/aot/external/LibObjectFile/changelog.md Outdated
@filipnavara

filipnavara commented Sep 30, 2023

Copy link
Copy Markdown
MemberAuthor

I think the code now reached a point where it passes the smoke tests for the supported platforms, which was the primary purpose of this PR. I'll keep it open for the moment, but it served its purpose.

Aside from some general structural improvements these are the areas I intend to explore next (in no particular order):

  • Emitting DWARF debugging info eagerly and directly into corresponding sections without the intermediate representation. (partially done, on par with LLVM ObjWriter)
  • Resolve relative relocations within same section before writing them down to the object files. This should have no effect on output after linking. It does, however, produce slightly smaller object files, and it may avoid hitting limits on the number of relocations in certain formats.
  • Optimize the string table building to take advantage of suffix matches. All the formats use some variation of string table with null-terminated string referenced by offset. It's possible to generate smaller object files by producing _unwind0_XYZ at offset N, and then reusing the suffix XYZ at offset N+9 as a different string.
  • Explore generating XDATA/PDATA unwinding info in COFF to be closer to the structure that MSVC produces. (done)
  • Implement COMDAT handling for ELF file format. It's the one big missing thing for feature parity. (done)

@am11

am11 commented Oct 1, 2023

Copy link
Copy Markdown
Member

According to dotnet-trace on linux-arm64. ilc.dll spends > 11 seconds in

libobjectfile!LibObjectFile.Elf.ElfWriter`1[LibObjectFile.Elf.ElfEncoderDirect].WriteSections()

during System.Runtime.Tests publishing (and output object size is 273M vs. main's 244M). With published-ilc, this test OOMs (code 137, the current CI failure).

speedscope-trace.zip

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@am11 Thanks for looking into it, really appreciated!

I didn't focus on the performance and memory usage outside of isolated scenarios. I profiled some code paths for COFF and CodeView but there's very little overlap with what ELF and DWARF does. I'll check the trace you provided and do some profiling on my side as well.

The output size difference is expected. It's mostly caused by extra relocations in the output and non-optimized string table (as mentioned in the "future work" list above). There's also some difference in debugging info size but not nearly as big.

Notably, the memory usage of the DWARF debugging info emitter is pretty high. It's not easy to refactor without significant changes to LibObjectFile. It operates on a "document" model (akin to JsonDocument but binary and multiple cross-linked documents), while we really need something closer to the "writer" model (akin to Utf8JsonWriter). There's already an internal model present in the compiler and it is memory intensive to convert from one model to the other. Additionally, the DWARF model in LibObjectFile has additional abstraction for relocations which are then converted to ELF relocations. This all adds up both in memory and time profiles.

I did some comparisons with the debugging info turned off, and the results were largely favorable to the C# implementation. That said, I did it primarily on Mach-O, not ELF.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

speedscope-trace.zip

Wow, I totally didn't expect ElfObjectFile.Verify to take 5 times as much as ElfObjectFile.WriteSections. I knew ElfStringTable.GetOrCreateIndex is extremely inefficient but this exceeded all my expectations. It tries to do suffix matching but in the process creates a ton of strings. Given all the time spent in the method I suspect the memory usage culprit could be there as well.

@filipnavara

filipnavara commented Oct 2, 2023

Copy link
Copy Markdown
MemberAuthor

Since some people are apparently following the PR and looking at some of the performance issues, I made an isolated sample showing the problem with ElfStringTable: https://github.com/filipnavara/StringTableBenchmark

I used three different algorithms to build the string table:

  • Naive produces string table with no suffix deduplication (similar to the code used for COFF)
  • MultiKeySort sorts the input strings and then produces optimized string table with deduplicated suffixes (similar to the LLVM string table builder)
  • ElfStringTable uses LibObjectFile's ElfStringTable, which incrementally builds the table and uses dictionary to deduplicate suffixes

Note that ElfStringTable technically provides an incremental API where the full set of string is not known beforehand. However, in reality it's pre-populated with the full string set anyway, and this property can be used for optimizing the common case without changing the external API.

Without further ado, here are the results on my MacBook Air M1 for a sample string set taken from UseSystemResourceKeys.o in the smoke tests. The set contains slightly less than 25k strings where many can be suffix compressed.

MethodMeanErrorStdDevGen0Gen1Gen2Allocated
ElfStringTable350.838 ms6.8811 ms8.1914 ms38000.000021000.00007000.0000316.81 MB
MultiKeySort11.456 ms0.0469 ms0.0439 ms1062.50001046.8750968.75004.87 MB
Naive2.374 ms0.0060 ms0.0050 ms1042.96881015.6250996.09385.82 MB

You can clearly see that the approach used by ElfStringTable doesn't scale well.

--

The input is pre-sorted, so the 3-way radix quick sort in MultiKeySort happens to choose unoptimal pivot, which skews the result a bit. I didn't spend much time micro-optimizing it, so it serves more as a ballpark figure.

--

I updated the MultiKeySort version a bit to get 30% speed improvement. The size of the output is 2,457,077 bytes for the Naive implementation, and 1,896,752 for the MultiKeySort one.

@filipnavara
filipnavaraforce-pushed the objwriter3 branch 2 times, most recently from 00bfd74 to 1c0d928CompareOctober 2, 2023 16:06
@am11

am11 commented Oct 2, 2023

Copy link
Copy Markdown
Member

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

@agocke

Copy link
Copy Markdown
Member

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

I really appreciate that you checked and helped diagnose this issue. 👍 I am really happy to have some working baseline version of the changes before I proceed to do further optimizations and experiments.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

I committed an experimental fix and it passed the CI. I'll submit it upstream and then focus on the further work I mentioned above.

@TIHan

TIHan commented Oct 2, 2023

Copy link
Copy Markdown
Contributor

@filipnavara , this is looking really good and glad others were able to look at it. The performance improvements do look great.

How far do you think your solution is from matching the existing functionality? It looks like the smoke tests are passing on all the platforms. @agocke , are there any scenarios that we need to cover that are not covered by the tests?

- Section names need to come first in the string table because of limited space for their reference by offset. This caused the "managedcode$I" and "modules$I" section names to be garbage when there were many symbols.
- Fix missing array pool return.
Remove LibObjectFile dependency
foreach (var sequencePoint in sequencePoints)
{
if (lastFileName == null || lastFileName != sequencePoint.FileName)
if (lastFileName is null || lastFileName != sequencePoint.FileName)

@am11am11Dec 11, 2023

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.

If you prefer pattern matching terse syntax, we can reduce the verbosity a bit in those long RelocType.XX conditions:

usingstaticILCompiler.DependencyAnalysis.RelocType;
...if(relocTypeisIMAGE_REL_BASED_ARM64_BRANCH26 or IMAGE_REL_BASED_ARM64_PAGEBASE_REL21 or
IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A or IMAGE_REL_AARCH64_TLSLE_ADD_TPREL_HI12 or ..)
...

(the duplication due to IMAGE_REL_ prefix is enough as-is 😅)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I used several different code styles as I progressed, sometimes intentionally, sometimes as a consequence of reusing existing code... I am generally open to ideas how to keep the code as terse and readable as possible ;-)

@am11am11 mentioned this pull request Jan 8, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jan 11, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrcommunity-contributionIndicates that the PR has been added by a community memberNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)NO-REVIEWExperimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@filipnavara@am11@TIHan@agocke@SingleAccretion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[CI ONLY] [NativeAOT] ObjWriter in C# - #92705

Closed
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3
Closed

[CI ONLY] [NativeAOT] ObjWriter in C##92705
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3

Conversation

@filipnavara

@filipnavarafilipnavara commented Sep 27, 2023

Copy link
Copy Markdown
Member

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Tracking list of issues found by the CI:

  • linux-x64: System.NotSupportedException: Unsupported relocation: IMAGE_REL_TLSGD
  • linux-arm64: relocation R_AARCH64_TLSDESC_ADR_PAGE21 cannot be used against symbol 'tls_InlinedThreadStatics'
  • linux-arm64: libunwind: malformed DW_CFA_register DWARF unwind, reg too big
  • linux-arm64: Different pointer encoding is used in EH frames which seems to cause issues
  • iOS/tvOS: warning: ignoring file /tmp/helix/working/B5450932/w/C6420AD0/e/publish/native/iOS.Device.Aot.Test.o, building for iOS-arm64 but attempting to link with file built for unknown-unsupported file format ( 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 )
  • win-x64: Framework.lib(System.Private.CoreLib.obj) : fatal error LNK1243: invalid or corrupt file: COMDAT section 0x20001 associated with following section 0x0 (MultiModule test)
  • win-arm64: CoreFXTestLibrary.AssertTestException: Assert.AreEqual: Expected: [System.Func2[CommonType10[],CommonType10[]]]. Actual: [System.Func2[CommonType10[],CommonType10[]]].

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.Reflection.Metadata labels Sep 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

area-System.Reflection.Metadata, community-contribution

Milestone:-

@ghost

Copy link
Copy Markdown

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

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

community-contribution, area-NativeAOT-coreclr

Milestone:-

@filipnavarafilipnavara added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it labels Sep 27, 2023
@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

@am11

am11 commented Sep 28, 2023

Copy link
Copy Markdown
Member

This one looks a bit less mysterious:

Running Test: ExistingInstantiations.Test.TestWithExistingInst
Caught Unexpected exception:System.NotSupportedException: Cannot retrieve a MethodInfo for this delegate because the method it targeted was not enabled for metadata.
at Internal.Reflection.Extensions.NonPortable.DelegateMethodInfoRetriever.GetDelegateMethodInfo(Delegate) + 0x29c
at ExistingInstantiations.Test.TestWithExistingInst() + 0x518
at CoreFXTestLibrary.Internal.Runner.RunTestMethod(TestInfo) + 0x364
at CoreFXTestLibrary.Internal.Runner.RunTest(TestInfo) + 0x20
---- Test FAILED ---------------

perhaps TestReverseLookupsWithArrayArg failure has the same underlying reason?

BTW, CI is using precisely this toolchain version:

 -- The C compiler identification is MSVC 19.36.32537.0
-- The CXX compiler identification is MSVC 19.36.32537.0

to cross-compile for arm64 on x64: build.cmd -ci -arch arm64 -os windows -s clr.aot+host.native+libs+tools.illink -c Release -rc Release -lc Release -hc Release

Comment threadsrc/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj Outdated
@filipnavara

Copy link
Copy Markdown
MemberAuthor

This one looks a bit less mysterious:

Thanks, @am11. I will check that one. I will likely wait till tomorrow to have direct access to the Win/ARM machine instead of just RDP-over-RDP-over-Tailscale. I have an uncommitted code where I match more closely the COFF output from the old ObjWriter so it's diffable to certain extent, and easier to spot differences. The DynamicGenerics test produces high number of sections and relocations and triggers the "big obj" code paths which didn't get much testing, so that's my primary suspicion.

@TIHan

Copy link
Copy Markdown
Contributor

Thank you for making the PR @filipnavara .

I'm out for a bit and hopefully will look at this next week. So far it looks promising.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

Turns out the error was somewhere between the chair and the keyboard. I had a wrong branch checked out.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

I found the issue in the COFF/ARM64 code. I was incorrectly ignoring the addend for IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A (and it was actually an Assert on debug build). This broke fat pointers that use the +2 address offset to be distinguished from real pointers. The same issue didn't happen for Mach-O since it rewrites this particular relocation into a more complex one. On ELF we emit the RELA section with addend inside the relocations, not inside the code, so it happened to work. COFF is the only platform that embeds this kind of relocation in the data.

While searching for the root cause I found couple more issues that are mostly harmless but should be fixed nevertheless. I'll clean up the code and commit it soon.

Comment threadsrc/coreclr/tools/aot/external/LibObjectFile/changelog.md Outdated
@filipnavara

filipnavara commented Sep 30, 2023

Copy link
Copy Markdown
MemberAuthor

I think the code now reached a point where it passes the smoke tests for the supported platforms, which was the primary purpose of this PR. I'll keep it open for the moment, but it served its purpose.

Aside from some general structural improvements these are the areas I intend to explore next (in no particular order):

  • Emitting DWARF debugging info eagerly and directly into corresponding sections without the intermediate representation. (partially done, on par with LLVM ObjWriter)
  • Resolve relative relocations within same section before writing them down to the object files. This should have no effect on output after linking. It does, however, produce slightly smaller object files, and it may avoid hitting limits on the number of relocations in certain formats.
  • Optimize the string table building to take advantage of suffix matches. All the formats use some variation of string table with null-terminated string referenced by offset. It's possible to generate smaller object files by producing _unwind0_XYZ at offset N, and then reusing the suffix XYZ at offset N+9 as a different string.
  • Explore generating XDATA/PDATA unwinding info in COFF to be closer to the structure that MSVC produces. (done)
  • Implement COMDAT handling for ELF file format. It's the one big missing thing for feature parity. (done)

@am11

am11 commented Oct 1, 2023

Copy link
Copy Markdown
Member

According to dotnet-trace on linux-arm64. ilc.dll spends > 11 seconds in

libobjectfile!LibObjectFile.Elf.ElfWriter`1[LibObjectFile.Elf.ElfEncoderDirect].WriteSections()

during System.Runtime.Tests publishing (and output object size is 273M vs. main's 244M). With published-ilc, this test OOMs (code 137, the current CI failure).

speedscope-trace.zip

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@am11 Thanks for looking into it, really appreciated!

I didn't focus on the performance and memory usage outside of isolated scenarios. I profiled some code paths for COFF and CodeView but there's very little overlap with what ELF and DWARF does. I'll check the trace you provided and do some profiling on my side as well.

The output size difference is expected. It's mostly caused by extra relocations in the output and non-optimized string table (as mentioned in the "future work" list above). There's also some difference in debugging info size but not nearly as big.

Notably, the memory usage of the DWARF debugging info emitter is pretty high. It's not easy to refactor without significant changes to LibObjectFile. It operates on a "document" model (akin to JsonDocument but binary and multiple cross-linked documents), while we really need something closer to the "writer" model (akin to Utf8JsonWriter). There's already an internal model present in the compiler and it is memory intensive to convert from one model to the other. Additionally, the DWARF model in LibObjectFile has additional abstraction for relocations which are then converted to ELF relocations. This all adds up both in memory and time profiles.

I did some comparisons with the debugging info turned off, and the results were largely favorable to the C# implementation. That said, I did it primarily on Mach-O, not ELF.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

speedscope-trace.zip

Wow, I totally didn't expect ElfObjectFile.Verify to take 5 times as much as ElfObjectFile.WriteSections. I knew ElfStringTable.GetOrCreateIndex is extremely inefficient but this exceeded all my expectations. It tries to do suffix matching but in the process creates a ton of strings. Given all the time spent in the method I suspect the memory usage culprit could be there as well.

@filipnavara

filipnavara commented Oct 2, 2023

Copy link
Copy Markdown
MemberAuthor

Since some people are apparently following the PR and looking at some of the performance issues, I made an isolated sample showing the problem with ElfStringTable: https://github.com/filipnavara/StringTableBenchmark

I used three different algorithms to build the string table:

  • Naive produces string table with no suffix deduplication (similar to the code used for COFF)
  • MultiKeySort sorts the input strings and then produces optimized string table with deduplicated suffixes (similar to the LLVM string table builder)
  • ElfStringTable uses LibObjectFile's ElfStringTable, which incrementally builds the table and uses dictionary to deduplicate suffixes

Note that ElfStringTable technically provides an incremental API where the full set of string is not known beforehand. However, in reality it's pre-populated with the full string set anyway, and this property can be used for optimizing the common case without changing the external API.

Without further ado, here are the results on my MacBook Air M1 for a sample string set taken from UseSystemResourceKeys.o in the smoke tests. The set contains slightly less than 25k strings where many can be suffix compressed.

MethodMeanErrorStdDevGen0Gen1Gen2Allocated
ElfStringTable350.838 ms6.8811 ms8.1914 ms38000.000021000.00007000.0000316.81 MB
MultiKeySort11.456 ms0.0469 ms0.0439 ms1062.50001046.8750968.75004.87 MB
Naive2.374 ms0.0060 ms0.0050 ms1042.96881015.6250996.09385.82 MB

You can clearly see that the approach used by ElfStringTable doesn't scale well.

--

The input is pre-sorted, so the 3-way radix quick sort in MultiKeySort happens to choose unoptimal pivot, which skews the result a bit. I didn't spend much time micro-optimizing it, so it serves more as a ballpark figure.

--

I updated the MultiKeySort version a bit to get 30% speed improvement. The size of the output is 2,457,077 bytes for the Naive implementation, and 1,896,752 for the MultiKeySort one.

@filipnavara
filipnavaraforce-pushed the objwriter3 branch 2 times, most recently from 00bfd74 to 1c0d928CompareOctober 2, 2023 16:06
@am11

am11 commented Oct 2, 2023

Copy link
Copy Markdown
Member

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

@agocke

Copy link
Copy Markdown
Member

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

I really appreciate that you checked and helped diagnose this issue. 👍 I am really happy to have some working baseline version of the changes before I proceed to do further optimizations and experiments.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

I committed an experimental fix and it passed the CI. I'll submit it upstream and then focus on the further work I mentioned above.

@TIHan

TIHan commented Oct 2, 2023

Copy link
Copy Markdown
Contributor

@filipnavara , this is looking really good and glad others were able to look at it. The performance improvements do look great.

How far do you think your solution is from matching the existing functionality? It looks like the smoke tests are passing on all the platforms. @agocke , are there any scenarios that we need to cover that are not covered by the tests?

- Section names need to come first in the string table because of limited space for their reference by offset. This caused the "managedcode$I" and "modules$I" section names to be garbage when there were many symbols.
- Fix missing array pool return.
Remove LibObjectFile dependency
foreach (var sequencePoint in sequencePoints)
{
if (lastFileName == null || lastFileName != sequencePoint.FileName)
if (lastFileName is null || lastFileName != sequencePoint.FileName)

@am11am11Dec 11, 2023

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.

If you prefer pattern matching terse syntax, we can reduce the verbosity a bit in those long RelocType.XX conditions:

usingstaticILCompiler.DependencyAnalysis.RelocType;
...if(relocTypeisIMAGE_REL_BASED_ARM64_BRANCH26 or IMAGE_REL_BASED_ARM64_PAGEBASE_REL21 or
IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A or IMAGE_REL_AARCH64_TLSLE_ADD_TPREL_HI12 or ..)
...

(the duplication due to IMAGE_REL_ prefix is enough as-is 😅)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I used several different code styles as I progressed, sometimes intentionally, sometimes as a consequence of reusing existing code... I am generally open to ideas how to keep the code as terse and readable as possible ;-)

@am11am11 mentioned this pull request Jan 8, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jan 11, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrcommunity-contributionIndicates that the PR has been added by a community memberNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)NO-REVIEWExperimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@filipnavara@am11@TIHan@agocke@SingleAccretion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[CI ONLY] [NativeAOT] ObjWriter in C# - #92705

Closed
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3
Closed

[CI ONLY] [NativeAOT] ObjWriter in C##92705
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3

Conversation

@filipnavara

@filipnavarafilipnavara commented Sep 27, 2023

Copy link
Copy Markdown
Member

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Tracking list of issues found by the CI:

  • linux-x64: System.NotSupportedException: Unsupported relocation: IMAGE_REL_TLSGD
  • linux-arm64: relocation R_AARCH64_TLSDESC_ADR_PAGE21 cannot be used against symbol 'tls_InlinedThreadStatics'
  • linux-arm64: libunwind: malformed DW_CFA_register DWARF unwind, reg too big
  • linux-arm64: Different pointer encoding is used in EH frames which seems to cause issues
  • iOS/tvOS: warning: ignoring file /tmp/helix/working/B5450932/w/C6420AD0/e/publish/native/iOS.Device.Aot.Test.o, building for iOS-arm64 but attempting to link with file built for unknown-unsupported file format ( 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 )
  • win-x64: Framework.lib(System.Private.CoreLib.obj) : fatal error LNK1243: invalid or corrupt file: COMDAT section 0x20001 associated with following section 0x0 (MultiModule test)
  • win-arm64: CoreFXTestLibrary.AssertTestException: Assert.AreEqual: Expected: [System.Func2[CommonType10[],CommonType10[]]]. Actual: [System.Func2[CommonType10[],CommonType10[]]].

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.Reflection.Metadata labels Sep 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

area-System.Reflection.Metadata, community-contribution

Milestone:-

@ghost

Copy link
Copy Markdown

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

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

community-contribution, area-NativeAOT-coreclr

Milestone:-

@filipnavarafilipnavara added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it labels Sep 27, 2023
@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

@am11

am11 commented Sep 28, 2023

Copy link
Copy Markdown
Member

This one looks a bit less mysterious:

Running Test: ExistingInstantiations.Test.TestWithExistingInst
Caught Unexpected exception:System.NotSupportedException: Cannot retrieve a MethodInfo for this delegate because the method it targeted was not enabled for metadata.
at Internal.Reflection.Extensions.NonPortable.DelegateMethodInfoRetriever.GetDelegateMethodInfo(Delegate) + 0x29c
at ExistingInstantiations.Test.TestWithExistingInst() + 0x518
at CoreFXTestLibrary.Internal.Runner.RunTestMethod(TestInfo) + 0x364
at CoreFXTestLibrary.Internal.Runner.RunTest(TestInfo) + 0x20
---- Test FAILED ---------------

perhaps TestReverseLookupsWithArrayArg failure has the same underlying reason?

BTW, CI is using precisely this toolchain version:

 -- The C compiler identification is MSVC 19.36.32537.0
-- The CXX compiler identification is MSVC 19.36.32537.0

to cross-compile for arm64 on x64: build.cmd -ci -arch arm64 -os windows -s clr.aot+host.native+libs+tools.illink -c Release -rc Release -lc Release -hc Release

Comment threadsrc/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj Outdated
@filipnavara

Copy link
Copy Markdown
MemberAuthor

This one looks a bit less mysterious:

Thanks, @am11. I will check that one. I will likely wait till tomorrow to have direct access to the Win/ARM machine instead of just RDP-over-RDP-over-Tailscale. I have an uncommitted code where I match more closely the COFF output from the old ObjWriter so it's diffable to certain extent, and easier to spot differences. The DynamicGenerics test produces high number of sections and relocations and triggers the "big obj" code paths which didn't get much testing, so that's my primary suspicion.

@TIHan

Copy link
Copy Markdown
Contributor

Thank you for making the PR @filipnavara .

I'm out for a bit and hopefully will look at this next week. So far it looks promising.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

Turns out the error was somewhere between the chair and the keyboard. I had a wrong branch checked out.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

I found the issue in the COFF/ARM64 code. I was incorrectly ignoring the addend for IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A (and it was actually an Assert on debug build). This broke fat pointers that use the +2 address offset to be distinguished from real pointers. The same issue didn't happen for Mach-O since it rewrites this particular relocation into a more complex one. On ELF we emit the RELA section with addend inside the relocations, not inside the code, so it happened to work. COFF is the only platform that embeds this kind of relocation in the data.

While searching for the root cause I found couple more issues that are mostly harmless but should be fixed nevertheless. I'll clean up the code and commit it soon.

Comment threadsrc/coreclr/tools/aot/external/LibObjectFile/changelog.md Outdated
@filipnavara

filipnavara commented Sep 30, 2023

Copy link
Copy Markdown
MemberAuthor

I think the code now reached a point where it passes the smoke tests for the supported platforms, which was the primary purpose of this PR. I'll keep it open for the moment, but it served its purpose.

Aside from some general structural improvements these are the areas I intend to explore next (in no particular order):

  • Emitting DWARF debugging info eagerly and directly into corresponding sections without the intermediate representation. (partially done, on par with LLVM ObjWriter)
  • Resolve relative relocations within same section before writing them down to the object files. This should have no effect on output after linking. It does, however, produce slightly smaller object files, and it may avoid hitting limits on the number of relocations in certain formats.
  • Optimize the string table building to take advantage of suffix matches. All the formats use some variation of string table with null-terminated string referenced by offset. It's possible to generate smaller object files by producing _unwind0_XYZ at offset N, and then reusing the suffix XYZ at offset N+9 as a different string.
  • Explore generating XDATA/PDATA unwinding info in COFF to be closer to the structure that MSVC produces. (done)
  • Implement COMDAT handling for ELF file format. It's the one big missing thing for feature parity. (done)

@am11

am11 commented Oct 1, 2023

Copy link
Copy Markdown
Member

According to dotnet-trace on linux-arm64. ilc.dll spends > 11 seconds in

libobjectfile!LibObjectFile.Elf.ElfWriter`1[LibObjectFile.Elf.ElfEncoderDirect].WriteSections()

during System.Runtime.Tests publishing (and output object size is 273M vs. main's 244M). With published-ilc, this test OOMs (code 137, the current CI failure).

speedscope-trace.zip

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@am11 Thanks for looking into it, really appreciated!

I didn't focus on the performance and memory usage outside of isolated scenarios. I profiled some code paths for COFF and CodeView but there's very little overlap with what ELF and DWARF does. I'll check the trace you provided and do some profiling on my side as well.

The output size difference is expected. It's mostly caused by extra relocations in the output and non-optimized string table (as mentioned in the "future work" list above). There's also some difference in debugging info size but not nearly as big.

Notably, the memory usage of the DWARF debugging info emitter is pretty high. It's not easy to refactor without significant changes to LibObjectFile. It operates on a "document" model (akin to JsonDocument but binary and multiple cross-linked documents), while we really need something closer to the "writer" model (akin to Utf8JsonWriter). There's already an internal model present in the compiler and it is memory intensive to convert from one model to the other. Additionally, the DWARF model in LibObjectFile has additional abstraction for relocations which are then converted to ELF relocations. This all adds up both in memory and time profiles.

I did some comparisons with the debugging info turned off, and the results were largely favorable to the C# implementation. That said, I did it primarily on Mach-O, not ELF.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

speedscope-trace.zip

Wow, I totally didn't expect ElfObjectFile.Verify to take 5 times as much as ElfObjectFile.WriteSections. I knew ElfStringTable.GetOrCreateIndex is extremely inefficient but this exceeded all my expectations. It tries to do suffix matching but in the process creates a ton of strings. Given all the time spent in the method I suspect the memory usage culprit could be there as well.

@filipnavara

filipnavara commented Oct 2, 2023

Copy link
Copy Markdown
MemberAuthor

Since some people are apparently following the PR and looking at some of the performance issues, I made an isolated sample showing the problem with ElfStringTable: https://github.com/filipnavara/StringTableBenchmark

I used three different algorithms to build the string table:

  • Naive produces string table with no suffix deduplication (similar to the code used for COFF)
  • MultiKeySort sorts the input strings and then produces optimized string table with deduplicated suffixes (similar to the LLVM string table builder)
  • ElfStringTable uses LibObjectFile's ElfStringTable, which incrementally builds the table and uses dictionary to deduplicate suffixes

Note that ElfStringTable technically provides an incremental API where the full set of string is not known beforehand. However, in reality it's pre-populated with the full string set anyway, and this property can be used for optimizing the common case without changing the external API.

Without further ado, here are the results on my MacBook Air M1 for a sample string set taken from UseSystemResourceKeys.o in the smoke tests. The set contains slightly less than 25k strings where many can be suffix compressed.

MethodMeanErrorStdDevGen0Gen1Gen2Allocated
ElfStringTable350.838 ms6.8811 ms8.1914 ms38000.000021000.00007000.0000316.81 MB
MultiKeySort11.456 ms0.0469 ms0.0439 ms1062.50001046.8750968.75004.87 MB
Naive2.374 ms0.0060 ms0.0050 ms1042.96881015.6250996.09385.82 MB

You can clearly see that the approach used by ElfStringTable doesn't scale well.

--

The input is pre-sorted, so the 3-way radix quick sort in MultiKeySort happens to choose unoptimal pivot, which skews the result a bit. I didn't spend much time micro-optimizing it, so it serves more as a ballpark figure.

--

I updated the MultiKeySort version a bit to get 30% speed improvement. The size of the output is 2,457,077 bytes for the Naive implementation, and 1,896,752 for the MultiKeySort one.

@filipnavara
filipnavaraforce-pushed the objwriter3 branch 2 times, most recently from 00bfd74 to 1c0d928CompareOctober 2, 2023 16:06
@am11

am11 commented Oct 2, 2023

Copy link
Copy Markdown
Member

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

@agocke

Copy link
Copy Markdown
Member

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

I really appreciate that you checked and helped diagnose this issue. 👍 I am really happy to have some working baseline version of the changes before I proceed to do further optimizations and experiments.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

I committed an experimental fix and it passed the CI. I'll submit it upstream and then focus on the further work I mentioned above.

@TIHan

TIHan commented Oct 2, 2023

Copy link
Copy Markdown
Contributor

@filipnavara , this is looking really good and glad others were able to look at it. The performance improvements do look great.

How far do you think your solution is from matching the existing functionality? It looks like the smoke tests are passing on all the platforms. @agocke , are there any scenarios that we need to cover that are not covered by the tests?

- Section names need to come first in the string table because of limited space for their reference by offset. This caused the "managedcode$I" and "modules$I" section names to be garbage when there were many symbols.
- Fix missing array pool return.
Remove LibObjectFile dependency
foreach (var sequencePoint in sequencePoints)
{
if (lastFileName == null || lastFileName != sequencePoint.FileName)
if (lastFileName is null || lastFileName != sequencePoint.FileName)

@am11am11Dec 11, 2023

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.

If you prefer pattern matching terse syntax, we can reduce the verbosity a bit in those long RelocType.XX conditions:

usingstaticILCompiler.DependencyAnalysis.RelocType;
...if(relocTypeisIMAGE_REL_BASED_ARM64_BRANCH26 or IMAGE_REL_BASED_ARM64_PAGEBASE_REL21 or
IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A or IMAGE_REL_AARCH64_TLSLE_ADD_TPREL_HI12 or ..)
...

(the duplication due to IMAGE_REL_ prefix is enough as-is 😅)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I used several different code styles as I progressed, sometimes intentionally, sometimes as a consequence of reusing existing code... I am generally open to ideas how to keep the code as terse and readable as possible ;-)

@am11am11 mentioned this pull request Jan 8, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jan 11, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrcommunity-contributionIndicates that the PR has been added by a community memberNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)NO-REVIEWExperimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@filipnavara@am11@TIHan@agocke@SingleAccretion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[CI ONLY] [NativeAOT] ObjWriter in C# - #92705

Closed
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3
Closed

[CI ONLY] [NativeAOT] ObjWriter in C##92705
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3

Conversation

@filipnavara

@filipnavarafilipnavara commented Sep 27, 2023

Copy link
Copy Markdown
Member

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Tracking list of issues found by the CI:

  • linux-x64: System.NotSupportedException: Unsupported relocation: IMAGE_REL_TLSGD
  • linux-arm64: relocation R_AARCH64_TLSDESC_ADR_PAGE21 cannot be used against symbol 'tls_InlinedThreadStatics'
  • linux-arm64: libunwind: malformed DW_CFA_register DWARF unwind, reg too big
  • linux-arm64: Different pointer encoding is used in EH frames which seems to cause issues
  • iOS/tvOS: warning: ignoring file /tmp/helix/working/B5450932/w/C6420AD0/e/publish/native/iOS.Device.Aot.Test.o, building for iOS-arm64 but attempting to link with file built for unknown-unsupported file format ( 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 )
  • win-x64: Framework.lib(System.Private.CoreLib.obj) : fatal error LNK1243: invalid or corrupt file: COMDAT section 0x20001 associated with following section 0x0 (MultiModule test)
  • win-arm64: CoreFXTestLibrary.AssertTestException: Assert.AreEqual: Expected: [System.Func2[CommonType10[],CommonType10[]]]. Actual: [System.Func2[CommonType10[],CommonType10[]]].

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.Reflection.Metadata labels Sep 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

area-System.Reflection.Metadata, community-contribution

Milestone:-

@ghost

Copy link
Copy Markdown

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

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

community-contribution, area-NativeAOT-coreclr

Milestone:-

@filipnavarafilipnavara added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it labels Sep 27, 2023
@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

@am11

am11 commented Sep 28, 2023

Copy link
Copy Markdown
Member

This one looks a bit less mysterious:

Running Test: ExistingInstantiations.Test.TestWithExistingInst
Caught Unexpected exception:System.NotSupportedException: Cannot retrieve a MethodInfo for this delegate because the method it targeted was not enabled for metadata.
at Internal.Reflection.Extensions.NonPortable.DelegateMethodInfoRetriever.GetDelegateMethodInfo(Delegate) + 0x29c
at ExistingInstantiations.Test.TestWithExistingInst() + 0x518
at CoreFXTestLibrary.Internal.Runner.RunTestMethod(TestInfo) + 0x364
at CoreFXTestLibrary.Internal.Runner.RunTest(TestInfo) + 0x20
---- Test FAILED ---------------

perhaps TestReverseLookupsWithArrayArg failure has the same underlying reason?

BTW, CI is using precisely this toolchain version:

 -- The C compiler identification is MSVC 19.36.32537.0
-- The CXX compiler identification is MSVC 19.36.32537.0

to cross-compile for arm64 on x64: build.cmd -ci -arch arm64 -os windows -s clr.aot+host.native+libs+tools.illink -c Release -rc Release -lc Release -hc Release

Comment threadsrc/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj Outdated
@filipnavara

Copy link
Copy Markdown
MemberAuthor

This one looks a bit less mysterious:

Thanks, @am11. I will check that one. I will likely wait till tomorrow to have direct access to the Win/ARM machine instead of just RDP-over-RDP-over-Tailscale. I have an uncommitted code where I match more closely the COFF output from the old ObjWriter so it's diffable to certain extent, and easier to spot differences. The DynamicGenerics test produces high number of sections and relocations and triggers the "big obj" code paths which didn't get much testing, so that's my primary suspicion.

@TIHan

Copy link
Copy Markdown
Contributor

Thank you for making the PR @filipnavara .

I'm out for a bit and hopefully will look at this next week. So far it looks promising.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

Turns out the error was somewhere between the chair and the keyboard. I had a wrong branch checked out.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

I found the issue in the COFF/ARM64 code. I was incorrectly ignoring the addend for IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A (and it was actually an Assert on debug build). This broke fat pointers that use the +2 address offset to be distinguished from real pointers. The same issue didn't happen for Mach-O since it rewrites this particular relocation into a more complex one. On ELF we emit the RELA section with addend inside the relocations, not inside the code, so it happened to work. COFF is the only platform that embeds this kind of relocation in the data.

While searching for the root cause I found couple more issues that are mostly harmless but should be fixed nevertheless. I'll clean up the code and commit it soon.

Comment threadsrc/coreclr/tools/aot/external/LibObjectFile/changelog.md Outdated
@filipnavara

filipnavara commented Sep 30, 2023

Copy link
Copy Markdown
MemberAuthor

I think the code now reached a point where it passes the smoke tests for the supported platforms, which was the primary purpose of this PR. I'll keep it open for the moment, but it served its purpose.

Aside from some general structural improvements these are the areas I intend to explore next (in no particular order):

  • Emitting DWARF debugging info eagerly and directly into corresponding sections without the intermediate representation. (partially done, on par with LLVM ObjWriter)
  • Resolve relative relocations within same section before writing them down to the object files. This should have no effect on output after linking. It does, however, produce slightly smaller object files, and it may avoid hitting limits on the number of relocations in certain formats.
  • Optimize the string table building to take advantage of suffix matches. All the formats use some variation of string table with null-terminated string referenced by offset. It's possible to generate smaller object files by producing _unwind0_XYZ at offset N, and then reusing the suffix XYZ at offset N+9 as a different string.
  • Explore generating XDATA/PDATA unwinding info in COFF to be closer to the structure that MSVC produces. (done)
  • Implement COMDAT handling for ELF file format. It's the one big missing thing for feature parity. (done)

@am11

am11 commented Oct 1, 2023

Copy link
Copy Markdown
Member

According to dotnet-trace on linux-arm64. ilc.dll spends > 11 seconds in

libobjectfile!LibObjectFile.Elf.ElfWriter`1[LibObjectFile.Elf.ElfEncoderDirect].WriteSections()

during System.Runtime.Tests publishing (and output object size is 273M vs. main's 244M). With published-ilc, this test OOMs (code 137, the current CI failure).

speedscope-trace.zip

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@am11 Thanks for looking into it, really appreciated!

I didn't focus on the performance and memory usage outside of isolated scenarios. I profiled some code paths for COFF and CodeView but there's very little overlap with what ELF and DWARF does. I'll check the trace you provided and do some profiling on my side as well.

The output size difference is expected. It's mostly caused by extra relocations in the output and non-optimized string table (as mentioned in the "future work" list above). There's also some difference in debugging info size but not nearly as big.

Notably, the memory usage of the DWARF debugging info emitter is pretty high. It's not easy to refactor without significant changes to LibObjectFile. It operates on a "document" model (akin to JsonDocument but binary and multiple cross-linked documents), while we really need something closer to the "writer" model (akin to Utf8JsonWriter). There's already an internal model present in the compiler and it is memory intensive to convert from one model to the other. Additionally, the DWARF model in LibObjectFile has additional abstraction for relocations which are then converted to ELF relocations. This all adds up both in memory and time profiles.

I did some comparisons with the debugging info turned off, and the results were largely favorable to the C# implementation. That said, I did it primarily on Mach-O, not ELF.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

speedscope-trace.zip

Wow, I totally didn't expect ElfObjectFile.Verify to take 5 times as much as ElfObjectFile.WriteSections. I knew ElfStringTable.GetOrCreateIndex is extremely inefficient but this exceeded all my expectations. It tries to do suffix matching but in the process creates a ton of strings. Given all the time spent in the method I suspect the memory usage culprit could be there as well.

@filipnavara

filipnavara commented Oct 2, 2023

Copy link
Copy Markdown
MemberAuthor

Since some people are apparently following the PR and looking at some of the performance issues, I made an isolated sample showing the problem with ElfStringTable: https://github.com/filipnavara/StringTableBenchmark

I used three different algorithms to build the string table:

  • Naive produces string table with no suffix deduplication (similar to the code used for COFF)
  • MultiKeySort sorts the input strings and then produces optimized string table with deduplicated suffixes (similar to the LLVM string table builder)
  • ElfStringTable uses LibObjectFile's ElfStringTable, which incrementally builds the table and uses dictionary to deduplicate suffixes

Note that ElfStringTable technically provides an incremental API where the full set of string is not known beforehand. However, in reality it's pre-populated with the full string set anyway, and this property can be used for optimizing the common case without changing the external API.

Without further ado, here are the results on my MacBook Air M1 for a sample string set taken from UseSystemResourceKeys.o in the smoke tests. The set contains slightly less than 25k strings where many can be suffix compressed.

MethodMeanErrorStdDevGen0Gen1Gen2Allocated
ElfStringTable350.838 ms6.8811 ms8.1914 ms38000.000021000.00007000.0000316.81 MB
MultiKeySort11.456 ms0.0469 ms0.0439 ms1062.50001046.8750968.75004.87 MB
Naive2.374 ms0.0060 ms0.0050 ms1042.96881015.6250996.09385.82 MB

You can clearly see that the approach used by ElfStringTable doesn't scale well.

--

The input is pre-sorted, so the 3-way radix quick sort in MultiKeySort happens to choose unoptimal pivot, which skews the result a bit. I didn't spend much time micro-optimizing it, so it serves more as a ballpark figure.

--

I updated the MultiKeySort version a bit to get 30% speed improvement. The size of the output is 2,457,077 bytes for the Naive implementation, and 1,896,752 for the MultiKeySort one.

@filipnavara
filipnavaraforce-pushed the objwriter3 branch 2 times, most recently from 00bfd74 to 1c0d928CompareOctober 2, 2023 16:06
@am11

am11 commented Oct 2, 2023

Copy link
Copy Markdown
Member

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

@agocke

Copy link
Copy Markdown
Member

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

I really appreciate that you checked and helped diagnose this issue. 👍 I am really happy to have some working baseline version of the changes before I proceed to do further optimizations and experiments.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

I committed an experimental fix and it passed the CI. I'll submit it upstream and then focus on the further work I mentioned above.

@TIHan

TIHan commented Oct 2, 2023

Copy link
Copy Markdown
Contributor

@filipnavara , this is looking really good and glad others were able to look at it. The performance improvements do look great.

How far do you think your solution is from matching the existing functionality? It looks like the smoke tests are passing on all the platforms. @agocke , are there any scenarios that we need to cover that are not covered by the tests?

- Section names need to come first in the string table because of limited space for their reference by offset. This caused the "managedcode$I" and "modules$I" section names to be garbage when there were many symbols.
- Fix missing array pool return.
Remove LibObjectFile dependency
foreach (var sequencePoint in sequencePoints)
{
if (lastFileName == null || lastFileName != sequencePoint.FileName)
if (lastFileName is null || lastFileName != sequencePoint.FileName)

@am11am11Dec 11, 2023

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.

If you prefer pattern matching terse syntax, we can reduce the verbosity a bit in those long RelocType.XX conditions:

usingstaticILCompiler.DependencyAnalysis.RelocType;
...if(relocTypeisIMAGE_REL_BASED_ARM64_BRANCH26 or IMAGE_REL_BASED_ARM64_PAGEBASE_REL21 or
IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A or IMAGE_REL_AARCH64_TLSLE_ADD_TPREL_HI12 or ..)
...

(the duplication due to IMAGE_REL_ prefix is enough as-is 😅)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I used several different code styles as I progressed, sometimes intentionally, sometimes as a consequence of reusing existing code... I am generally open to ideas how to keep the code as terse and readable as possible ;-)

@am11am11 mentioned this pull request Jan 8, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jan 11, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrcommunity-contributionIndicates that the PR has been added by a community memberNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)NO-REVIEWExperimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@filipnavara@am11@TIHan@agocke@SingleAccretion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[CI ONLY] [NativeAOT] ObjWriter in C# - #92705

Closed
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3
Closed

[CI ONLY] [NativeAOT] ObjWriter in C##92705
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3

Conversation

@filipnavara

@filipnavarafilipnavara commented Sep 27, 2023

Copy link
Copy Markdown
Member

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Tracking list of issues found by the CI:

  • linux-x64: System.NotSupportedException: Unsupported relocation: IMAGE_REL_TLSGD
  • linux-arm64: relocation R_AARCH64_TLSDESC_ADR_PAGE21 cannot be used against symbol 'tls_InlinedThreadStatics'
  • linux-arm64: libunwind: malformed DW_CFA_register DWARF unwind, reg too big
  • linux-arm64: Different pointer encoding is used in EH frames which seems to cause issues
  • iOS/tvOS: warning: ignoring file /tmp/helix/working/B5450932/w/C6420AD0/e/publish/native/iOS.Device.Aot.Test.o, building for iOS-arm64 but attempting to link with file built for unknown-unsupported file format ( 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 )
  • win-x64: Framework.lib(System.Private.CoreLib.obj) : fatal error LNK1243: invalid or corrupt file: COMDAT section 0x20001 associated with following section 0x0 (MultiModule test)
  • win-arm64: CoreFXTestLibrary.AssertTestException: Assert.AreEqual: Expected: [System.Func2[CommonType10[],CommonType10[]]]. Actual: [System.Func2[CommonType10[],CommonType10[]]].

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.Reflection.Metadata labels Sep 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

area-System.Reflection.Metadata, community-contribution

Milestone:-

@ghost

Copy link
Copy Markdown

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

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

community-contribution, area-NativeAOT-coreclr

Milestone:-

@filipnavarafilipnavara added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it labels Sep 27, 2023
@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

@am11

am11 commented Sep 28, 2023

Copy link
Copy Markdown
Member

This one looks a bit less mysterious:

Running Test: ExistingInstantiations.Test.TestWithExistingInst
Caught Unexpected exception:System.NotSupportedException: Cannot retrieve a MethodInfo for this delegate because the method it targeted was not enabled for metadata.
at Internal.Reflection.Extensions.NonPortable.DelegateMethodInfoRetriever.GetDelegateMethodInfo(Delegate) + 0x29c
at ExistingInstantiations.Test.TestWithExistingInst() + 0x518
at CoreFXTestLibrary.Internal.Runner.RunTestMethod(TestInfo) + 0x364
at CoreFXTestLibrary.Internal.Runner.RunTest(TestInfo) + 0x20
---- Test FAILED ---------------

perhaps TestReverseLookupsWithArrayArg failure has the same underlying reason?

BTW, CI is using precisely this toolchain version:

 -- The C compiler identification is MSVC 19.36.32537.0
-- The CXX compiler identification is MSVC 19.36.32537.0

to cross-compile for arm64 on x64: build.cmd -ci -arch arm64 -os windows -s clr.aot+host.native+libs+tools.illink -c Release -rc Release -lc Release -hc Release

Comment threadsrc/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj Outdated
@filipnavara

Copy link
Copy Markdown
MemberAuthor

This one looks a bit less mysterious:

Thanks, @am11. I will check that one. I will likely wait till tomorrow to have direct access to the Win/ARM machine instead of just RDP-over-RDP-over-Tailscale. I have an uncommitted code where I match more closely the COFF output from the old ObjWriter so it's diffable to certain extent, and easier to spot differences. The DynamicGenerics test produces high number of sections and relocations and triggers the "big obj" code paths which didn't get much testing, so that's my primary suspicion.

@TIHan

Copy link
Copy Markdown
Contributor

Thank you for making the PR @filipnavara .

I'm out for a bit and hopefully will look at this next week. So far it looks promising.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

Turns out the error was somewhere between the chair and the keyboard. I had a wrong branch checked out.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

I found the issue in the COFF/ARM64 code. I was incorrectly ignoring the addend for IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A (and it was actually an Assert on debug build). This broke fat pointers that use the +2 address offset to be distinguished from real pointers. The same issue didn't happen for Mach-O since it rewrites this particular relocation into a more complex one. On ELF we emit the RELA section with addend inside the relocations, not inside the code, so it happened to work. COFF is the only platform that embeds this kind of relocation in the data.

While searching for the root cause I found couple more issues that are mostly harmless but should be fixed nevertheless. I'll clean up the code and commit it soon.

Comment threadsrc/coreclr/tools/aot/external/LibObjectFile/changelog.md Outdated
@filipnavara

filipnavara commented Sep 30, 2023

Copy link
Copy Markdown
MemberAuthor

I think the code now reached a point where it passes the smoke tests for the supported platforms, which was the primary purpose of this PR. I'll keep it open for the moment, but it served its purpose.

Aside from some general structural improvements these are the areas I intend to explore next (in no particular order):

  • Emitting DWARF debugging info eagerly and directly into corresponding sections without the intermediate representation. (partially done, on par with LLVM ObjWriter)
  • Resolve relative relocations within same section before writing them down to the object files. This should have no effect on output after linking. It does, however, produce slightly smaller object files, and it may avoid hitting limits on the number of relocations in certain formats.
  • Optimize the string table building to take advantage of suffix matches. All the formats use some variation of string table with null-terminated string referenced by offset. It's possible to generate smaller object files by producing _unwind0_XYZ at offset N, and then reusing the suffix XYZ at offset N+9 as a different string.
  • Explore generating XDATA/PDATA unwinding info in COFF to be closer to the structure that MSVC produces. (done)
  • Implement COMDAT handling for ELF file format. It's the one big missing thing for feature parity. (done)

@am11

am11 commented Oct 1, 2023

Copy link
Copy Markdown
Member

According to dotnet-trace on linux-arm64. ilc.dll spends > 11 seconds in

libobjectfile!LibObjectFile.Elf.ElfWriter`1[LibObjectFile.Elf.ElfEncoderDirect].WriteSections()

during System.Runtime.Tests publishing (and output object size is 273M vs. main's 244M). With published-ilc, this test OOMs (code 137, the current CI failure).

speedscope-trace.zip

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@am11 Thanks for looking into it, really appreciated!

I didn't focus on the performance and memory usage outside of isolated scenarios. I profiled some code paths for COFF and CodeView but there's very little overlap with what ELF and DWARF does. I'll check the trace you provided and do some profiling on my side as well.

The output size difference is expected. It's mostly caused by extra relocations in the output and non-optimized string table (as mentioned in the "future work" list above). There's also some difference in debugging info size but not nearly as big.

Notably, the memory usage of the DWARF debugging info emitter is pretty high. It's not easy to refactor without significant changes to LibObjectFile. It operates on a "document" model (akin to JsonDocument but binary and multiple cross-linked documents), while we really need something closer to the "writer" model (akin to Utf8JsonWriter). There's already an internal model present in the compiler and it is memory intensive to convert from one model to the other. Additionally, the DWARF model in LibObjectFile has additional abstraction for relocations which are then converted to ELF relocations. This all adds up both in memory and time profiles.

I did some comparisons with the debugging info turned off, and the results were largely favorable to the C# implementation. That said, I did it primarily on Mach-O, not ELF.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

speedscope-trace.zip

Wow, I totally didn't expect ElfObjectFile.Verify to take 5 times as much as ElfObjectFile.WriteSections. I knew ElfStringTable.GetOrCreateIndex is extremely inefficient but this exceeded all my expectations. It tries to do suffix matching but in the process creates a ton of strings. Given all the time spent in the method I suspect the memory usage culprit could be there as well.

@filipnavara

filipnavara commented Oct 2, 2023

Copy link
Copy Markdown
MemberAuthor

Since some people are apparently following the PR and looking at some of the performance issues, I made an isolated sample showing the problem with ElfStringTable: https://github.com/filipnavara/StringTableBenchmark

I used three different algorithms to build the string table:

  • Naive produces string table with no suffix deduplication (similar to the code used for COFF)
  • MultiKeySort sorts the input strings and then produces optimized string table with deduplicated suffixes (similar to the LLVM string table builder)
  • ElfStringTable uses LibObjectFile's ElfStringTable, which incrementally builds the table and uses dictionary to deduplicate suffixes

Note that ElfStringTable technically provides an incremental API where the full set of string is not known beforehand. However, in reality it's pre-populated with the full string set anyway, and this property can be used for optimizing the common case without changing the external API.

Without further ado, here are the results on my MacBook Air M1 for a sample string set taken from UseSystemResourceKeys.o in the smoke tests. The set contains slightly less than 25k strings where many can be suffix compressed.

MethodMeanErrorStdDevGen0Gen1Gen2Allocated
ElfStringTable350.838 ms6.8811 ms8.1914 ms38000.000021000.00007000.0000316.81 MB
MultiKeySort11.456 ms0.0469 ms0.0439 ms1062.50001046.8750968.75004.87 MB
Naive2.374 ms0.0060 ms0.0050 ms1042.96881015.6250996.09385.82 MB

You can clearly see that the approach used by ElfStringTable doesn't scale well.

--

The input is pre-sorted, so the 3-way radix quick sort in MultiKeySort happens to choose unoptimal pivot, which skews the result a bit. I didn't spend much time micro-optimizing it, so it serves more as a ballpark figure.

--

I updated the MultiKeySort version a bit to get 30% speed improvement. The size of the output is 2,457,077 bytes for the Naive implementation, and 1,896,752 for the MultiKeySort one.

@filipnavara
filipnavaraforce-pushed the objwriter3 branch 2 times, most recently from 00bfd74 to 1c0d928CompareOctober 2, 2023 16:06
@am11

am11 commented Oct 2, 2023

Copy link
Copy Markdown
Member

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

@agocke

Copy link
Copy Markdown
Member

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

I really appreciate that you checked and helped diagnose this issue. 👍 I am really happy to have some working baseline version of the changes before I proceed to do further optimizations and experiments.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

I committed an experimental fix and it passed the CI. I'll submit it upstream and then focus on the further work I mentioned above.

@TIHan

TIHan commented Oct 2, 2023

Copy link
Copy Markdown
Contributor

@filipnavara , this is looking really good and glad others were able to look at it. The performance improvements do look great.

How far do you think your solution is from matching the existing functionality? It looks like the smoke tests are passing on all the platforms. @agocke , are there any scenarios that we need to cover that are not covered by the tests?

- Section names need to come first in the string table because of limited space for their reference by offset. This caused the "managedcode$I" and "modules$I" section names to be garbage when there were many symbols.
- Fix missing array pool return.
Remove LibObjectFile dependency
foreach (var sequencePoint in sequencePoints)
{
if (lastFileName == null || lastFileName != sequencePoint.FileName)
if (lastFileName is null || lastFileName != sequencePoint.FileName)

@am11am11Dec 11, 2023

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.

If you prefer pattern matching terse syntax, we can reduce the verbosity a bit in those long RelocType.XX conditions:

usingstaticILCompiler.DependencyAnalysis.RelocType;
...if(relocTypeisIMAGE_REL_BASED_ARM64_BRANCH26 or IMAGE_REL_BASED_ARM64_PAGEBASE_REL21 or
IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A or IMAGE_REL_AARCH64_TLSLE_ADD_TPREL_HI12 or ..)
...

(the duplication due to IMAGE_REL_ prefix is enough as-is 😅)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I used several different code styles as I progressed, sometimes intentionally, sometimes as a consequence of reusing existing code... I am generally open to ideas how to keep the code as terse and readable as possible ;-)

@am11am11 mentioned this pull request Jan 8, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jan 11, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrcommunity-contributionIndicates that the PR has been added by a community memberNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)NO-REVIEWExperimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@filipnavara@am11@TIHan@agocke@SingleAccretion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[CI ONLY] [NativeAOT] ObjWriter in C# - #92705

Closed
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3
Closed

[CI ONLY] [NativeAOT] ObjWriter in C##92705
filipnavara wants to merge 120 commits into
dotnet:mainfrom
filipnavara:objwriter3

Conversation

@filipnavara

@filipnavarafilipnavara commented Sep 27, 2023

Copy link
Copy Markdown
Member

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Tracking list of issues found by the CI:

  • linux-x64: System.NotSupportedException: Unsupported relocation: IMAGE_REL_TLSGD
  • linux-arm64: relocation R_AARCH64_TLSDESC_ADR_PAGE21 cannot be used against symbol 'tls_InlinedThreadStatics'
  • linux-arm64: libunwind: malformed DW_CFA_register DWARF unwind, reg too big
  • linux-arm64: Different pointer encoding is used in EH frames which seems to cause issues
  • iOS/tvOS: warning: ignoring file /tmp/helix/working/B5450932/w/C6420AD0/e/publish/native/iOS.Device.Aot.Test.o, building for iOS-arm64 but attempting to link with file built for unknown-unsupported file format ( 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 )
  • win-x64: Framework.lib(System.Private.CoreLib.obj) : fatal error LNK1243: invalid or corrupt file: COMDAT section 0x20001 associated with following section 0x0 (MultiModule test)
  • win-arm64: CoreFXTestLibrary.AssertTestException: Assert.AreEqual: Expected: [System.Func2[CommonType10[],CommonType10[]]]. Actual: [System.Func2[CommonType10[],CommonType10[]]].

@ghostghost added community-contribution Indicates that the PR has been added by a community member area-System.Reflection.Metadata labels Sep 27, 2023
@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

area-System.Reflection.Metadata, community-contribution

Milestone:-

@ghost

Copy link
Copy Markdown

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

Issue Details

Ref: #77178

This is reimplementation of NativeAOT ObjWriter in pure C# instead of depending on LLVM. It implements Mach-O, ELF, COFF object file emitter with DWARF and CodeView debugging information. Only x64 and arm64 targets are implemented to cover officially supported platforms. Certain features are not implemented yet, e.g. COMDAT in ELF. Other features like DWARF debugging info generation are currently slower than the previous implementation. A limited testing was done on osx-arm64, win-x64, and linux-x64. Previous version of the branch was also tested on osx-x64, win-arm64, and linux-arm64.

Caveat: This is NOT for review, the draft PR is opened specifically to run smoke tests only. The code was rebased over current main branch and updated to reflect most ObjWriter changes from the past year (both on the runtime repo side and the LLVM fork repo). The performance and structure of the code is not the final shape and I expect to rewrite certain parts before submitting this for actual review.

cc @TIHan

Author:filipnavara
Assignees:-
Labels:

community-contribution, area-NativeAOT-coreclr

Milestone:-

@filipnavarafilipnavara added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it labels Sep 27, 2023
@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

@am11

am11 commented Sep 28, 2023

Copy link
Copy Markdown
Member

This one looks a bit less mysterious:

Running Test: ExistingInstantiations.Test.TestWithExistingInst
Caught Unexpected exception:System.NotSupportedException: Cannot retrieve a MethodInfo for this delegate because the method it targeted was not enabled for metadata.
at Internal.Reflection.Extensions.NonPortable.DelegateMethodInfoRetriever.GetDelegateMethodInfo(Delegate) + 0x29c
at ExistingInstantiations.Test.TestWithExistingInst() + 0x518
at CoreFXTestLibrary.Internal.Runner.RunTestMethod(TestInfo) + 0x364
at CoreFXTestLibrary.Internal.Runner.RunTest(TestInfo) + 0x20
---- Test FAILED ---------------

perhaps TestReverseLookupsWithArrayArg failure has the same underlying reason?

BTW, CI is using precisely this toolchain version:

 -- The C compiler identification is MSVC 19.36.32537.0
-- The CXX compiler identification is MSVC 19.36.32537.0

to cross-compile for arm64 on x64: build.cmd -ci -arch arm64 -os windows -s clr.aot+host.native+libs+tools.illink -c Release -rc Release -lc Release -hc Release

Comment threadsrc/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj Outdated
@filipnavara

Copy link
Copy Markdown
MemberAuthor

This one looks a bit less mysterious:

Thanks, @am11. I will check that one. I will likely wait till tomorrow to have direct access to the Win/ARM machine instead of just RDP-over-RDP-over-Tailscale. I have an uncommitted code where I match more closely the COFF output from the old ObjWriter so it's diffable to certain extent, and easier to spot differences. The DynamicGenerics test produces high number of sections and relocations and triggers the "big obj" code paths which didn't get much testing, so that's my primary suspicion.

@TIHan

Copy link
Copy Markdown
Contributor

Thank you for making the PR @filipnavara .

I'm out for a bit and hopefully will look at this next week. So far it looks promising.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Still not sure what is going on with the win-arm64 tests. They pass locally on my Windows Dev Kit 2023.

Turns out the error was somewhere between the chair and the keyboard. I had a wrong branch checked out.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

I found the issue in the COFF/ARM64 code. I was incorrectly ignoring the addend for IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A (and it was actually an Assert on debug build). This broke fat pointers that use the +2 address offset to be distinguished from real pointers. The same issue didn't happen for Mach-O since it rewrites this particular relocation into a more complex one. On ELF we emit the RELA section with addend inside the relocations, not inside the code, so it happened to work. COFF is the only platform that embeds this kind of relocation in the data.

While searching for the root cause I found couple more issues that are mostly harmless but should be fixed nevertheless. I'll clean up the code and commit it soon.

Comment threadsrc/coreclr/tools/aot/external/LibObjectFile/changelog.md Outdated
@filipnavara

filipnavara commented Sep 30, 2023

Copy link
Copy Markdown
MemberAuthor

I think the code now reached a point where it passes the smoke tests for the supported platforms, which was the primary purpose of this PR. I'll keep it open for the moment, but it served its purpose.

Aside from some general structural improvements these are the areas I intend to explore next (in no particular order):

  • Emitting DWARF debugging info eagerly and directly into corresponding sections without the intermediate representation. (partially done, on par with LLVM ObjWriter)
  • Resolve relative relocations within same section before writing them down to the object files. This should have no effect on output after linking. It does, however, produce slightly smaller object files, and it may avoid hitting limits on the number of relocations in certain formats.
  • Optimize the string table building to take advantage of suffix matches. All the formats use some variation of string table with null-terminated string referenced by offset. It's possible to generate smaller object files by producing _unwind0_XYZ at offset N, and then reusing the suffix XYZ at offset N+9 as a different string.
  • Explore generating XDATA/PDATA unwinding info in COFF to be closer to the structure that MSVC produces. (done)
  • Implement COMDAT handling for ELF file format. It's the one big missing thing for feature parity. (done)

@am11

am11 commented Oct 1, 2023

Copy link
Copy Markdown
Member

According to dotnet-trace on linux-arm64. ilc.dll spends > 11 seconds in

libobjectfile!LibObjectFile.Elf.ElfWriter`1[LibObjectFile.Elf.ElfEncoderDirect].WriteSections()

during System.Runtime.Tests publishing (and output object size is 273M vs. main's 244M). With published-ilc, this test OOMs (code 137, the current CI failure).

speedscope-trace.zip

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@am11 Thanks for looking into it, really appreciated!

I didn't focus on the performance and memory usage outside of isolated scenarios. I profiled some code paths for COFF and CodeView but there's very little overlap with what ELF and DWARF does. I'll check the trace you provided and do some profiling on my side as well.

The output size difference is expected. It's mostly caused by extra relocations in the output and non-optimized string table (as mentioned in the "future work" list above). There's also some difference in debugging info size but not nearly as big.

Notably, the memory usage of the DWARF debugging info emitter is pretty high. It's not easy to refactor without significant changes to LibObjectFile. It operates on a "document" model (akin to JsonDocument but binary and multiple cross-linked documents), while we really need something closer to the "writer" model (akin to Utf8JsonWriter). There's already an internal model present in the compiler and it is memory intensive to convert from one model to the other. Additionally, the DWARF model in LibObjectFile has additional abstraction for relocations which are then converted to ELF relocations. This all adds up both in memory and time profiles.

I did some comparisons with the debugging info turned off, and the results were largely favorable to the C# implementation. That said, I did it primarily on Mach-O, not ELF.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

speedscope-trace.zip

Wow, I totally didn't expect ElfObjectFile.Verify to take 5 times as much as ElfObjectFile.WriteSections. I knew ElfStringTable.GetOrCreateIndex is extremely inefficient but this exceeded all my expectations. It tries to do suffix matching but in the process creates a ton of strings. Given all the time spent in the method I suspect the memory usage culprit could be there as well.

@filipnavara

filipnavara commented Oct 2, 2023

Copy link
Copy Markdown
MemberAuthor

Since some people are apparently following the PR and looking at some of the performance issues, I made an isolated sample showing the problem with ElfStringTable: https://github.com/filipnavara/StringTableBenchmark

I used three different algorithms to build the string table:

  • Naive produces string table with no suffix deduplication (similar to the code used for COFF)
  • MultiKeySort sorts the input strings and then produces optimized string table with deduplicated suffixes (similar to the LLVM string table builder)
  • ElfStringTable uses LibObjectFile's ElfStringTable, which incrementally builds the table and uses dictionary to deduplicate suffixes

Note that ElfStringTable technically provides an incremental API where the full set of string is not known beforehand. However, in reality it's pre-populated with the full string set anyway, and this property can be used for optimizing the common case without changing the external API.

Without further ado, here are the results on my MacBook Air M1 for a sample string set taken from UseSystemResourceKeys.o in the smoke tests. The set contains slightly less than 25k strings where many can be suffix compressed.

MethodMeanErrorStdDevGen0Gen1Gen2Allocated
ElfStringTable350.838 ms6.8811 ms8.1914 ms38000.000021000.00007000.0000316.81 MB
MultiKeySort11.456 ms0.0469 ms0.0439 ms1062.50001046.8750968.75004.87 MB
Naive2.374 ms0.0060 ms0.0050 ms1042.96881015.6250996.09385.82 MB

You can clearly see that the approach used by ElfStringTable doesn't scale well.

--

The input is pre-sorted, so the 3-way radix quick sort in MultiKeySort happens to choose unoptimal pivot, which skews the result a bit. I didn't spend much time micro-optimizing it, so it serves more as a ballpark figure.

--

I updated the MultiKeySort version a bit to get 30% speed improvement. The size of the output is 2,457,077 bytes for the Naive implementation, and 1,896,752 for the MultiKeySort one.

@filipnavara
filipnavaraforce-pushed the objwriter3 branch 2 times, most recently from 00bfd74 to 1c0d928CompareOctober 2, 2023 16:06
@am11

am11 commented Oct 2, 2023

Copy link
Copy Markdown
Member

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

@agocke

Copy link
Copy Markdown
Member

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

@filipnavara

Copy link
Copy Markdown
MemberAuthor

Great optimizations! linux-arm64 object size is 241M vs. 244M on main, and CI leg isn't jamming. :)

I really appreciate that you checked and helped diagnose this issue. 👍 I am really happy to have some working baseline version of the changes before I proceed to do further optimizations and experiments.

@filipnavara

Copy link
Copy Markdown
MemberAuthor

@filipnavara really good info. Agreed that ElfStringTable isn't looking great

I committed an experimental fix and it passed the CI. I'll submit it upstream and then focus on the further work I mentioned above.

@TIHan

TIHan commented Oct 2, 2023

Copy link
Copy Markdown
Contributor

@filipnavara , this is looking really good and glad others were able to look at it. The performance improvements do look great.

How far do you think your solution is from matching the existing functionality? It looks like the smoke tests are passing on all the platforms. @agocke , are there any scenarios that we need to cover that are not covered by the tests?

- Section names need to come first in the string table because of limited space for their reference by offset. This caused the "managedcode$I" and "modules$I" section names to be garbage when there were many symbols.
- Fix missing array pool return.
Remove LibObjectFile dependency
foreach (var sequencePoint in sequencePoints)
{
if (lastFileName == null || lastFileName != sequencePoint.FileName)
if (lastFileName is null || lastFileName != sequencePoint.FileName)

@am11am11Dec 11, 2023

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.

If you prefer pattern matching terse syntax, we can reduce the verbosity a bit in those long RelocType.XX conditions:

usingstaticILCompiler.DependencyAnalysis.RelocType;
...if(relocTypeisIMAGE_REL_BASED_ARM64_BRANCH26 or IMAGE_REL_BASED_ARM64_PAGEBASE_REL21 or
IMAGE_REL_BASED_ARM64_PAGEOFFSET_12A or IMAGE_REL_AARCH64_TLSLE_ADD_TPREL_HI12 or ..)
...

(the duplication due to IMAGE_REL_ prefix is enough as-is 😅)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I used several different code styles as I progressed, sometimes intentionally, sometimes as a consequence of reusing existing code... I am generally open to ideas how to keep the code as terse and readable as possible ;-)

@am11am11 mentioned this pull request Jan 8, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jan 11, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-NativeAOT-coreclrcommunity-contributionIndicates that the PR has been added by a community memberNO-MERGEThe PR is not ready for merge yet (see discussion for detailed reasons)NO-REVIEWExperimental/testing PR, do NOT review it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@filipnavara@am11@TIHan@agocke@SingleAccretion