A few optimizations for the gcinfodecoder construction - #96150

Merged
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec
Dec 28, 2023
Merged

A few optimizations for the gcinfodecoder construction#96150
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec

Conversation

@VSadov

@VSadovVSadov commented Dec 18, 2023

Copy link
Copy Markdown
Member

Constructing gcinfodecoder performs some initial decoding. While the time spent in initial decoding is typically less than the time spent enumerating live slots, it is not insignificant. There are some opportunities to do initial decoding a bit cheaper.

Comment threadsrc/coreclr/inc/gcinfotypes.h Outdated
DWORD lzcountCeil;
_BitScanReverse(&lzcountCeil, (unsigned long)x);
#else // _MSC_VER
UINT32 lzcountCeil = (UINT32)__builtin_clz((unsigned int)x);

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.

I believe __builtin_clz already encodes the subtract from BITS_PER_SIZE_T within the intrinsic function unlike _BitScanReverse

@VSadovVSadovDec 19, 2023

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.

right, I've just realized that even though lzcnt is encoded similarly to bsr on x64, the result is an offset from different ends.

@VSadov

VSadov commented Dec 19, 2023

Copy link
Copy Markdown
MemberAuthor

To measure the impact I use the following microbenchmark compiled with NativeAOT
(NativeAOT is used to reduce impact/noise from suspension)

The numbers are averaged GC Gen0 pauses in milliseconds. Lower is better.
On x64, Windows10, AMD 5950X, 16 cores, 32 logical

I see ~ 20% improvement.

==== Before the change:

0.1528027343750001
0.15324121093750026
0.15360449218750016
0.14911621093750022
0.15105566406250034
0.15213964843750036
0.15154882812500012
. . .

=== After the change:

0.12535742187500004
0.12644238281249998
0.12518164062499954
0.12546582031250003
0.12549902343750005
0.12462207031249978
. . .

m_InitialRelPos = other.m_InitialRelPos;
m_pCurrent = other.m_pCurrent;
m_RelPos = other.m_RelPos;
m_current = other.m_current;

@VSadovVSadovDec 19, 2023

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.

On 64bit one native word can act as a "buffer" for quite a few reads when each read takes only a few bits. This change reduces the need for indirect reads from the bitstream and may allow the compiler to enregister the "buffer".


size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

The point of this change is to use a fixed-size shift, which is typically faster than a variable-sized shift.
Same applies to Read( int numBits ) when we read a fixed sized nibble.

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

@VSadovVSadovDec 26, 2023

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 will have to recheck the original codegen, but I think what was happening is that we would do indirect read and then apply a mask that was constructed via a variable shift of 1.
I guess that was because we need the result in a register and do not want to change the bit stream and the ways m_pCurrent and m_RelPos were changing did not allow to hoist/CSE/enregister either the result of the indirect read nor the computed mask.

@VSadovVSadovDec 26, 2023

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.

Interestingly for Zen3 the table gives no difference whatsoever between immediate and CL shift versions.

SHL, SHR, SAR r,i/CL | 1 | 1 | 0.5

yet profiler finds CL shift operations relatively expensive.
I often see that in other code, so I always assumed that varaible shifts are somewhat costly.

Maybe it is not the instruction itself, but the whole sequence of dependent instructions - load 1, load CL, make a mask, do a read, apply the mask, and sampling profiler just attributes most of that to the shift.

The code does get measurably faster after the change. I had a few other changes that did not improve anything so I did not include them, but this one definitely helped.

c++ inlines and interleaves statement parts quite aggressively, so it is a bit hard to see what belongs where, but I see that a few "expensive" CL shifts are gone.
Here is an example of what happened to a loop like:

for(UINT32i=0; i<numSlots; i++)
{
if(m_Reader.ReadOneFast())
numCouldBeLiveSlots++;
}

It is not an example of expensive/hot code, just something that is easier to read and see how codegen changed.

==== original code

00007FF700CE9C4E testr13d,r13d00007FF700CE9C51 je GcInfoDecoder::EnumerateLiveSlots+898h (07FF700CE9C98h) 00007FF700CE9C53 movr8d,r13d00007FF700CE9C56 nop word ptr [rax+rax]00007FF700CE9C60 movecx,r15d00007FF700CE9C63 movrax,r11 ; r11 has `1` in it, assigned outside of the loop00007FF700CE9C66 shlrax,cl ; depends on 2 instructions above00007FF700CE9C69 andrax,qword ptr [rdx] ; depends on instruction above + read (L1 is 3-5 cycles)00007FF700CE9C6C incr15d00007FF700CE9C6F mov dword ptr [rdi+18h],r15d00007FF700CE9C73 cmpr15d,40h00007FF700CE9C77 jne GcInfoDecoder::EnumerateLiveSlots+88Bh (07FF700CE9C8Bh) 00007FF700CE9C79 addrdx,8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF700CE9C7D mov dword ptr [rdi+18h],000007FF700CE9C84 mov qword ptr [rdi+10h],rdx00007FF700CE9C88 xorr15d,r15d00007FF700CE9C8B testrax,rax00007FF700CE9C8E je GcInfoDecoder::EnumerateLiveSlots+893h (07FF700CE9C93h) 00007FF700CE9C90 incr12d00007FF700CE9C93 subr8,r11

vs.

==== new

00007FF688D6A454 testr12d,r12d00007FF688D6A457 je GcInfoDecoder::EnumerateLiveSlots+0C04h (07FF688D6A4B4h) 00007FF688D6A459 movrax,r1100007FF688D6A45C movedx,r12d00007FF688D6A45F nop00007FF688D6A460 movrcx,rax00007FF688D6A463 incr8d ; can run together or even before the previous instruction00007FF688D6A466 shrrax,1 ; this and the next can run at the same time00007FF688D6A469 andecx,1 ; both only depend on "mov rcx,rax"00007FF688D6A46C mov qword ptr [rbx+20h],rax ; we do writes, but we do not need to wait for them00007FF688D6A470 mov dword ptr [rbx+18h],r8d00007FF688D6A474 mov qword ptr [rsp+48h],rax ; not sure what is stored here, in other cases we have just 2 writes00007FF688D6A479 cmpr8d,40h00007FF688D6A47D jne GcInfoDecoder::EnumerateLiveSlots+0BEBh (07FF688D6A49Bh) 00007FF688D6A47F add qword ptr [rbx+10h],8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF688D6A484 movrax,qword ptr [rbx+10h]00007FF688D6A488 xorr8d,r8d00007FF688D6A48B movrax,qword ptr [rax]00007FF688D6A48E mov qword ptr [rbx+20h],rax00007FF688D6A492 mov qword ptr [rsp+48h],rax00007FF688D6A497 mov dword ptr [rbx+18h],r8d00007FF688D6A49B testrcx,rcx00007FF688D6A49E je GcInfoDecoder::EnumerateLiveSlots+0BF3h (07FF688D6A4A3h) 00007FF688D6A4A0 incr13d00007FF688D6A4A3 subrdx,1

bool m_IsInterruptible;
bool m_IsVarArg;
bool m_GenericSecretParamIsMD;
bool m_GenericSecretParamIsMT;

@VSadovVSadovDec 19, 2023

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.

These are derived from masking the header flags. Masking is cheap and we may not even be asked for these, so we can do masking in the accessors.

// Use flag mask to bail out early if we already decoded all the pieces that caller requested
int remainingFlags = flags == DECODE_EVERYTHING ? ~0 : flags;

if (!slimHeader)

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.

Predecoding a fat header could be much more involved compared to slim headers, so let's deal with fat headers in a separate helper.

@VSadov

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@VSadov
VSadov marked this pull request as ready for review December 20, 2023 06:37
@VSadov

Copy link
Copy Markdown
MemberAuthor

I think this is ready for review.

@VSadov
VSadov requested a review from jkotasDecember 25, 2023 04:51
Comment threadsrc/coreclr/inc/gcinfodecoder.h

size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

Comment threadsrc/coreclr/inc/gcinfodecoder.h
if(m_NumSafePoints == 0)
return false;

#if defined(TARGET_AMD64) || defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)

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.

Removal of this ifdef is changing behavior for Linux x86. I guess that it is a bug fix. I do not see a reason why safepoint handling should be different for Linux x86.

cc @gbalykov

@VSadovVSadovDec 28, 2023

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 assumed we can't get here with x86 since we use a different gcinfo encoder.
If that is not the case and we can on Linux, I will put this back.

Overall, I think this -1 hack is trying to solve a problem that does not exist, and it would be better if we could stop doing/undoing this artificial adjustment, but it is not relevant to this PR. I just thought that #if defined is always true here.

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.

@jkotas thanks for sharing this. Currently almost all clr tests fail on linux x86 on main branch, so there're other issues with it that need to be investigated first. We'll probably take a look at this change after that.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thank you!

@VSadov

VSadov commented Dec 28, 2023

Copy link
Copy Markdown
MemberAuthor

It looks like the linux-arm64 Release NativeAOT_Libraries is stuck in a loop repeating the same:

 Done building Helix work items. Work item count: 9
Starting Azure Pipelines Test Run net9.0-linux-Release-arm64-NativeAOT_Release-(Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8
Job e739cf8d-0586-4722-b4a3-61c7ae9aaf97 on (Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8 is completed with 10 finished work items.
Using _DebuggerHosts: chrome
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
BuildTargetFramework: net9.0
TestArchiveTestsRoot: /__w/1/s/artifacts/helix/tests/
TestArchiveRoot: /__w/1/s/artifacts/helix/
TestArchiveRuntimeRoot: /__w/1/s/artifacts/helix/runtime/
TestArchiveRuntimeFile: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Compressing runtime directory
Creating directory /__w/1/s/artifacts/helix/runtime/
Zipping directory "/__w/1/s/artifacts/bin/testhost/net9.0-linux-Release-arm64/" to "/__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip".
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
Build TargetFramework: net9.0
Building Helix work items
Using TestRunNamePrefix: net9.0-linux-Release-arm64-NativeAOT_Release-
Using HelixCorrelationPayload: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Using HelixCommand: ./RunTests.sh --runtime-path "$HELIX_CORRELATION_PAYLOAD"
Using HelixType: test/functional/cli/innerloop/
Using WorkItemArchiveWildCard: /__w/1/s/artifacts/helix/tests/**/*.zip
Using Timeout: 00:45:00
Done building Helix work items. Work item count: 9

https://dev.azure.com/dnceng-public/public/_build/results?buildId=509866&view=logs&j=74bca3e6-08a5-5cef-0121-fe27fe79cc7b&t=317b4f3c-303e-557e-6c7e-66c36fe29909

It is not happening in my newer test run (#85694), so perhaps it is something that got fixed recently.

@VSadov
VSadov merged commit b4ba5da into dotnet:mainDec 28, 2023
@VSadov
VSadov deleted the dec branch December 28, 2023 21:44
@VSadov

Copy link
Copy Markdown
MemberAuthor

Thanks!!!

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 4, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrtenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@VSadov@jkotas@davidwrighton@gbalykov
, '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

A few optimizations for the gcinfodecoder construction - #96150

Merged
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec
Dec 28, 2023
Merged

A few optimizations for the gcinfodecoder construction#96150
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec

Conversation

@VSadov

@VSadovVSadov commented Dec 18, 2023

Copy link
Copy Markdown
Member

Constructing gcinfodecoder performs some initial decoding. While the time spent in initial decoding is typically less than the time spent enumerating live slots, it is not insignificant. There are some opportunities to do initial decoding a bit cheaper.

Comment threadsrc/coreclr/inc/gcinfotypes.h Outdated
DWORD lzcountCeil;
_BitScanReverse(&lzcountCeil, (unsigned long)x);
#else // _MSC_VER
UINT32 lzcountCeil = (UINT32)__builtin_clz((unsigned int)x);

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.

I believe __builtin_clz already encodes the subtract from BITS_PER_SIZE_T within the intrinsic function unlike _BitScanReverse

@VSadovVSadovDec 19, 2023

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.

right, I've just realized that even though lzcnt is encoded similarly to bsr on x64, the result is an offset from different ends.

@VSadov

VSadov commented Dec 19, 2023

Copy link
Copy Markdown
MemberAuthor

To measure the impact I use the following microbenchmark compiled with NativeAOT
(NativeAOT is used to reduce impact/noise from suspension)

The numbers are averaged GC Gen0 pauses in milliseconds. Lower is better.
On x64, Windows10, AMD 5950X, 16 cores, 32 logical

I see ~ 20% improvement.

==== Before the change:

0.1528027343750001
0.15324121093750026
0.15360449218750016
0.14911621093750022
0.15105566406250034
0.15213964843750036
0.15154882812500012
. . .

=== After the change:

0.12535742187500004
0.12644238281249998
0.12518164062499954
0.12546582031250003
0.12549902343750005
0.12462207031249978
. . .

m_InitialRelPos = other.m_InitialRelPos;
m_pCurrent = other.m_pCurrent;
m_RelPos = other.m_RelPos;
m_current = other.m_current;

@VSadovVSadovDec 19, 2023

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.

On 64bit one native word can act as a "buffer" for quite a few reads when each read takes only a few bits. This change reduces the need for indirect reads from the bitstream and may allow the compiler to enregister the "buffer".


size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

The point of this change is to use a fixed-size shift, which is typically faster than a variable-sized shift.
Same applies to Read( int numBits ) when we read a fixed sized nibble.

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

@VSadovVSadovDec 26, 2023

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 will have to recheck the original codegen, but I think what was happening is that we would do indirect read and then apply a mask that was constructed via a variable shift of 1.
I guess that was because we need the result in a register and do not want to change the bit stream and the ways m_pCurrent and m_RelPos were changing did not allow to hoist/CSE/enregister either the result of the indirect read nor the computed mask.

@VSadovVSadovDec 26, 2023

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.

Interestingly for Zen3 the table gives no difference whatsoever between immediate and CL shift versions.

SHL, SHR, SAR r,i/CL | 1 | 1 | 0.5

yet profiler finds CL shift operations relatively expensive.
I often see that in other code, so I always assumed that varaible shifts are somewhat costly.

Maybe it is not the instruction itself, but the whole sequence of dependent instructions - load 1, load CL, make a mask, do a read, apply the mask, and sampling profiler just attributes most of that to the shift.

The code does get measurably faster after the change. I had a few other changes that did not improve anything so I did not include them, but this one definitely helped.

c++ inlines and interleaves statement parts quite aggressively, so it is a bit hard to see what belongs where, but I see that a few "expensive" CL shifts are gone.
Here is an example of what happened to a loop like:

for(UINT32i=0; i<numSlots; i++)
{
if(m_Reader.ReadOneFast())
numCouldBeLiveSlots++;
}

It is not an example of expensive/hot code, just something that is easier to read and see how codegen changed.

==== original code

00007FF700CE9C4E testr13d,r13d00007FF700CE9C51 je GcInfoDecoder::EnumerateLiveSlots+898h (07FF700CE9C98h) 00007FF700CE9C53 movr8d,r13d00007FF700CE9C56 nop word ptr [rax+rax]00007FF700CE9C60 movecx,r15d00007FF700CE9C63 movrax,r11 ; r11 has `1` in it, assigned outside of the loop00007FF700CE9C66 shlrax,cl ; depends on 2 instructions above00007FF700CE9C69 andrax,qword ptr [rdx] ; depends on instruction above + read (L1 is 3-5 cycles)00007FF700CE9C6C incr15d00007FF700CE9C6F mov dword ptr [rdi+18h],r15d00007FF700CE9C73 cmpr15d,40h00007FF700CE9C77 jne GcInfoDecoder::EnumerateLiveSlots+88Bh (07FF700CE9C8Bh) 00007FF700CE9C79 addrdx,8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF700CE9C7D mov dword ptr [rdi+18h],000007FF700CE9C84 mov qword ptr [rdi+10h],rdx00007FF700CE9C88 xorr15d,r15d00007FF700CE9C8B testrax,rax00007FF700CE9C8E je GcInfoDecoder::EnumerateLiveSlots+893h (07FF700CE9C93h) 00007FF700CE9C90 incr12d00007FF700CE9C93 subr8,r11

vs.

==== new

00007FF688D6A454 testr12d,r12d00007FF688D6A457 je GcInfoDecoder::EnumerateLiveSlots+0C04h (07FF688D6A4B4h) 00007FF688D6A459 movrax,r1100007FF688D6A45C movedx,r12d00007FF688D6A45F nop00007FF688D6A460 movrcx,rax00007FF688D6A463 incr8d ; can run together or even before the previous instruction00007FF688D6A466 shrrax,1 ; this and the next can run at the same time00007FF688D6A469 andecx,1 ; both only depend on "mov rcx,rax"00007FF688D6A46C mov qword ptr [rbx+20h],rax ; we do writes, but we do not need to wait for them00007FF688D6A470 mov dword ptr [rbx+18h],r8d00007FF688D6A474 mov qword ptr [rsp+48h],rax ; not sure what is stored here, in other cases we have just 2 writes00007FF688D6A479 cmpr8d,40h00007FF688D6A47D jne GcInfoDecoder::EnumerateLiveSlots+0BEBh (07FF688D6A49Bh) 00007FF688D6A47F add qword ptr [rbx+10h],8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF688D6A484 movrax,qword ptr [rbx+10h]00007FF688D6A488 xorr8d,r8d00007FF688D6A48B movrax,qword ptr [rax]00007FF688D6A48E mov qword ptr [rbx+20h],rax00007FF688D6A492 mov qword ptr [rsp+48h],rax00007FF688D6A497 mov dword ptr [rbx+18h],r8d00007FF688D6A49B testrcx,rcx00007FF688D6A49E je GcInfoDecoder::EnumerateLiveSlots+0BF3h (07FF688D6A4A3h) 00007FF688D6A4A0 incr13d00007FF688D6A4A3 subrdx,1

bool m_IsInterruptible;
bool m_IsVarArg;
bool m_GenericSecretParamIsMD;
bool m_GenericSecretParamIsMT;

@VSadovVSadovDec 19, 2023

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.

These are derived from masking the header flags. Masking is cheap and we may not even be asked for these, so we can do masking in the accessors.

// Use flag mask to bail out early if we already decoded all the pieces that caller requested
int remainingFlags = flags == DECODE_EVERYTHING ? ~0 : flags;

if (!slimHeader)

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.

Predecoding a fat header could be much more involved compared to slim headers, so let's deal with fat headers in a separate helper.

@VSadov

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@VSadov
VSadov marked this pull request as ready for review December 20, 2023 06:37
@VSadov

Copy link
Copy Markdown
MemberAuthor

I think this is ready for review.

@VSadov
VSadov requested a review from jkotasDecember 25, 2023 04:51
Comment threadsrc/coreclr/inc/gcinfodecoder.h

size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

Comment threadsrc/coreclr/inc/gcinfodecoder.h
if(m_NumSafePoints == 0)
return false;

#if defined(TARGET_AMD64) || defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)

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.

Removal of this ifdef is changing behavior for Linux x86. I guess that it is a bug fix. I do not see a reason why safepoint handling should be different for Linux x86.

cc @gbalykov

@VSadovVSadovDec 28, 2023

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 assumed we can't get here with x86 since we use a different gcinfo encoder.
If that is not the case and we can on Linux, I will put this back.

Overall, I think this -1 hack is trying to solve a problem that does not exist, and it would be better if we could stop doing/undoing this artificial adjustment, but it is not relevant to this PR. I just thought that #if defined is always true here.

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.

@jkotas thanks for sharing this. Currently almost all clr tests fail on linux x86 on main branch, so there're other issues with it that need to be investigated first. We'll probably take a look at this change after that.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thank you!

@VSadov

VSadov commented Dec 28, 2023

Copy link
Copy Markdown
MemberAuthor

It looks like the linux-arm64 Release NativeAOT_Libraries is stuck in a loop repeating the same:

 Done building Helix work items. Work item count: 9
Starting Azure Pipelines Test Run net9.0-linux-Release-arm64-NativeAOT_Release-(Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8
Job e739cf8d-0586-4722-b4a3-61c7ae9aaf97 on (Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8 is completed with 10 finished work items.
Using _DebuggerHosts: chrome
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
BuildTargetFramework: net9.0
TestArchiveTestsRoot: /__w/1/s/artifacts/helix/tests/
TestArchiveRoot: /__w/1/s/artifacts/helix/
TestArchiveRuntimeRoot: /__w/1/s/artifacts/helix/runtime/
TestArchiveRuntimeFile: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Compressing runtime directory
Creating directory /__w/1/s/artifacts/helix/runtime/
Zipping directory "/__w/1/s/artifacts/bin/testhost/net9.0-linux-Release-arm64/" to "/__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip".
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
Build TargetFramework: net9.0
Building Helix work items
Using TestRunNamePrefix: net9.0-linux-Release-arm64-NativeAOT_Release-
Using HelixCorrelationPayload: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Using HelixCommand: ./RunTests.sh --runtime-path "$HELIX_CORRELATION_PAYLOAD"
Using HelixType: test/functional/cli/innerloop/
Using WorkItemArchiveWildCard: /__w/1/s/artifacts/helix/tests/**/*.zip
Using Timeout: 00:45:00
Done building Helix work items. Work item count: 9

https://dev.azure.com/dnceng-public/public/_build/results?buildId=509866&view=logs&j=74bca3e6-08a5-5cef-0121-fe27fe79cc7b&t=317b4f3c-303e-557e-6c7e-66c36fe29909

It is not happening in my newer test run (#85694), so perhaps it is something that got fixed recently.

@VSadov
VSadov merged commit b4ba5da into dotnet:mainDec 28, 2023
@VSadov
VSadov deleted the dec branch December 28, 2023 21:44
@VSadov

Copy link
Copy Markdown
MemberAuthor

Thanks!!!

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 4, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrtenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@VSadov@jkotas@davidwrighton@gbalykov
, '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

A few optimizations for the gcinfodecoder construction - #96150

Merged
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec
Dec 28, 2023
Merged

A few optimizations for the gcinfodecoder construction#96150
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec

Conversation

@VSadov

@VSadovVSadov commented Dec 18, 2023

Copy link
Copy Markdown
Member

Constructing gcinfodecoder performs some initial decoding. While the time spent in initial decoding is typically less than the time spent enumerating live slots, it is not insignificant. There are some opportunities to do initial decoding a bit cheaper.

Comment threadsrc/coreclr/inc/gcinfotypes.h Outdated
DWORD lzcountCeil;
_BitScanReverse(&lzcountCeil, (unsigned long)x);
#else // _MSC_VER
UINT32 lzcountCeil = (UINT32)__builtin_clz((unsigned int)x);

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.

I believe __builtin_clz already encodes the subtract from BITS_PER_SIZE_T within the intrinsic function unlike _BitScanReverse

@VSadovVSadovDec 19, 2023

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.

right, I've just realized that even though lzcnt is encoded similarly to bsr on x64, the result is an offset from different ends.

@VSadov

VSadov commented Dec 19, 2023

Copy link
Copy Markdown
MemberAuthor

To measure the impact I use the following microbenchmark compiled with NativeAOT
(NativeAOT is used to reduce impact/noise from suspension)

The numbers are averaged GC Gen0 pauses in milliseconds. Lower is better.
On x64, Windows10, AMD 5950X, 16 cores, 32 logical

I see ~ 20% improvement.

==== Before the change:

0.1528027343750001
0.15324121093750026
0.15360449218750016
0.14911621093750022
0.15105566406250034
0.15213964843750036
0.15154882812500012
. . .

=== After the change:

0.12535742187500004
0.12644238281249998
0.12518164062499954
0.12546582031250003
0.12549902343750005
0.12462207031249978
. . .

m_InitialRelPos = other.m_InitialRelPos;
m_pCurrent = other.m_pCurrent;
m_RelPos = other.m_RelPos;
m_current = other.m_current;

@VSadovVSadovDec 19, 2023

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.

On 64bit one native word can act as a "buffer" for quite a few reads when each read takes only a few bits. This change reduces the need for indirect reads from the bitstream and may allow the compiler to enregister the "buffer".


size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

The point of this change is to use a fixed-size shift, which is typically faster than a variable-sized shift.
Same applies to Read( int numBits ) when we read a fixed sized nibble.

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

@VSadovVSadovDec 26, 2023

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 will have to recheck the original codegen, but I think what was happening is that we would do indirect read and then apply a mask that was constructed via a variable shift of 1.
I guess that was because we need the result in a register and do not want to change the bit stream and the ways m_pCurrent and m_RelPos were changing did not allow to hoist/CSE/enregister either the result of the indirect read nor the computed mask.

@VSadovVSadovDec 26, 2023

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.

Interestingly for Zen3 the table gives no difference whatsoever between immediate and CL shift versions.

SHL, SHR, SAR r,i/CL | 1 | 1 | 0.5

yet profiler finds CL shift operations relatively expensive.
I often see that in other code, so I always assumed that varaible shifts are somewhat costly.

Maybe it is not the instruction itself, but the whole sequence of dependent instructions - load 1, load CL, make a mask, do a read, apply the mask, and sampling profiler just attributes most of that to the shift.

The code does get measurably faster after the change. I had a few other changes that did not improve anything so I did not include them, but this one definitely helped.

c++ inlines and interleaves statement parts quite aggressively, so it is a bit hard to see what belongs where, but I see that a few "expensive" CL shifts are gone.
Here is an example of what happened to a loop like:

for(UINT32i=0; i<numSlots; i++)
{
if(m_Reader.ReadOneFast())
numCouldBeLiveSlots++;
}

It is not an example of expensive/hot code, just something that is easier to read and see how codegen changed.

==== original code

00007FF700CE9C4E testr13d,r13d00007FF700CE9C51 je GcInfoDecoder::EnumerateLiveSlots+898h (07FF700CE9C98h) 00007FF700CE9C53 movr8d,r13d00007FF700CE9C56 nop word ptr [rax+rax]00007FF700CE9C60 movecx,r15d00007FF700CE9C63 movrax,r11 ; r11 has `1` in it, assigned outside of the loop00007FF700CE9C66 shlrax,cl ; depends on 2 instructions above00007FF700CE9C69 andrax,qword ptr [rdx] ; depends on instruction above + read (L1 is 3-5 cycles)00007FF700CE9C6C incr15d00007FF700CE9C6F mov dword ptr [rdi+18h],r15d00007FF700CE9C73 cmpr15d,40h00007FF700CE9C77 jne GcInfoDecoder::EnumerateLiveSlots+88Bh (07FF700CE9C8Bh) 00007FF700CE9C79 addrdx,8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF700CE9C7D mov dword ptr [rdi+18h],000007FF700CE9C84 mov qword ptr [rdi+10h],rdx00007FF700CE9C88 xorr15d,r15d00007FF700CE9C8B testrax,rax00007FF700CE9C8E je GcInfoDecoder::EnumerateLiveSlots+893h (07FF700CE9C93h) 00007FF700CE9C90 incr12d00007FF700CE9C93 subr8,r11

vs.

==== new

00007FF688D6A454 testr12d,r12d00007FF688D6A457 je GcInfoDecoder::EnumerateLiveSlots+0C04h (07FF688D6A4B4h) 00007FF688D6A459 movrax,r1100007FF688D6A45C movedx,r12d00007FF688D6A45F nop00007FF688D6A460 movrcx,rax00007FF688D6A463 incr8d ; can run together or even before the previous instruction00007FF688D6A466 shrrax,1 ; this and the next can run at the same time00007FF688D6A469 andecx,1 ; both only depend on "mov rcx,rax"00007FF688D6A46C mov qword ptr [rbx+20h],rax ; we do writes, but we do not need to wait for them00007FF688D6A470 mov dword ptr [rbx+18h],r8d00007FF688D6A474 mov qword ptr [rsp+48h],rax ; not sure what is stored here, in other cases we have just 2 writes00007FF688D6A479 cmpr8d,40h00007FF688D6A47D jne GcInfoDecoder::EnumerateLiveSlots+0BEBh (07FF688D6A49Bh) 00007FF688D6A47F add qword ptr [rbx+10h],8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF688D6A484 movrax,qword ptr [rbx+10h]00007FF688D6A488 xorr8d,r8d00007FF688D6A48B movrax,qword ptr [rax]00007FF688D6A48E mov qword ptr [rbx+20h],rax00007FF688D6A492 mov qword ptr [rsp+48h],rax00007FF688D6A497 mov dword ptr [rbx+18h],r8d00007FF688D6A49B testrcx,rcx00007FF688D6A49E je GcInfoDecoder::EnumerateLiveSlots+0BF3h (07FF688D6A4A3h) 00007FF688D6A4A0 incr13d00007FF688D6A4A3 subrdx,1

bool m_IsInterruptible;
bool m_IsVarArg;
bool m_GenericSecretParamIsMD;
bool m_GenericSecretParamIsMT;

@VSadovVSadovDec 19, 2023

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.

These are derived from masking the header flags. Masking is cheap and we may not even be asked for these, so we can do masking in the accessors.

// Use flag mask to bail out early if we already decoded all the pieces that caller requested
int remainingFlags = flags == DECODE_EVERYTHING ? ~0 : flags;

if (!slimHeader)

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.

Predecoding a fat header could be much more involved compared to slim headers, so let's deal with fat headers in a separate helper.

@VSadov

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@VSadov
VSadov marked this pull request as ready for review December 20, 2023 06:37
@VSadov

Copy link
Copy Markdown
MemberAuthor

I think this is ready for review.

@VSadov
VSadov requested a review from jkotasDecember 25, 2023 04:51
Comment threadsrc/coreclr/inc/gcinfodecoder.h

size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

Comment threadsrc/coreclr/inc/gcinfodecoder.h
if(m_NumSafePoints == 0)
return false;

#if defined(TARGET_AMD64) || defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)

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.

Removal of this ifdef is changing behavior for Linux x86. I guess that it is a bug fix. I do not see a reason why safepoint handling should be different for Linux x86.

cc @gbalykov

@VSadovVSadovDec 28, 2023

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 assumed we can't get here with x86 since we use a different gcinfo encoder.
If that is not the case and we can on Linux, I will put this back.

Overall, I think this -1 hack is trying to solve a problem that does not exist, and it would be better if we could stop doing/undoing this artificial adjustment, but it is not relevant to this PR. I just thought that #if defined is always true here.

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.

@jkotas thanks for sharing this. Currently almost all clr tests fail on linux x86 on main branch, so there're other issues with it that need to be investigated first. We'll probably take a look at this change after that.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thank you!

@VSadov

VSadov commented Dec 28, 2023

Copy link
Copy Markdown
MemberAuthor

It looks like the linux-arm64 Release NativeAOT_Libraries is stuck in a loop repeating the same:

 Done building Helix work items. Work item count: 9
Starting Azure Pipelines Test Run net9.0-linux-Release-arm64-NativeAOT_Release-(Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8
Job e739cf8d-0586-4722-b4a3-61c7ae9aaf97 on (Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8 is completed with 10 finished work items.
Using _DebuggerHosts: chrome
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
BuildTargetFramework: net9.0
TestArchiveTestsRoot: /__w/1/s/artifacts/helix/tests/
TestArchiveRoot: /__w/1/s/artifacts/helix/
TestArchiveRuntimeRoot: /__w/1/s/artifacts/helix/runtime/
TestArchiveRuntimeFile: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Compressing runtime directory
Creating directory /__w/1/s/artifacts/helix/runtime/
Zipping directory "/__w/1/s/artifacts/bin/testhost/net9.0-linux-Release-arm64/" to "/__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip".
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
Build TargetFramework: net9.0
Building Helix work items
Using TestRunNamePrefix: net9.0-linux-Release-arm64-NativeAOT_Release-
Using HelixCorrelationPayload: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Using HelixCommand: ./RunTests.sh --runtime-path "$HELIX_CORRELATION_PAYLOAD"
Using HelixType: test/functional/cli/innerloop/
Using WorkItemArchiveWildCard: /__w/1/s/artifacts/helix/tests/**/*.zip
Using Timeout: 00:45:00
Done building Helix work items. Work item count: 9

https://dev.azure.com/dnceng-public/public/_build/results?buildId=509866&view=logs&j=74bca3e6-08a5-5cef-0121-fe27fe79cc7b&t=317b4f3c-303e-557e-6c7e-66c36fe29909

It is not happening in my newer test run (#85694), so perhaps it is something that got fixed recently.

@VSadov
VSadov merged commit b4ba5da into dotnet:mainDec 28, 2023
@VSadov
VSadov deleted the dec branch December 28, 2023 21:44
@VSadov

Copy link
Copy Markdown
MemberAuthor

Thanks!!!

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 4, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrtenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@VSadov@jkotas@davidwrighton@gbalykov
, '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

A few optimizations for the gcinfodecoder construction - #96150

Merged
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec
Dec 28, 2023
Merged

A few optimizations for the gcinfodecoder construction#96150
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec

Conversation

@VSadov

@VSadovVSadov commented Dec 18, 2023

Copy link
Copy Markdown
Member

Constructing gcinfodecoder performs some initial decoding. While the time spent in initial decoding is typically less than the time spent enumerating live slots, it is not insignificant. There are some opportunities to do initial decoding a bit cheaper.

Comment threadsrc/coreclr/inc/gcinfotypes.h Outdated
DWORD lzcountCeil;
_BitScanReverse(&lzcountCeil, (unsigned long)x);
#else // _MSC_VER
UINT32 lzcountCeil = (UINT32)__builtin_clz((unsigned int)x);

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.

I believe __builtin_clz already encodes the subtract from BITS_PER_SIZE_T within the intrinsic function unlike _BitScanReverse

@VSadovVSadovDec 19, 2023

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.

right, I've just realized that even though lzcnt is encoded similarly to bsr on x64, the result is an offset from different ends.

@VSadov

VSadov commented Dec 19, 2023

Copy link
Copy Markdown
MemberAuthor

To measure the impact I use the following microbenchmark compiled with NativeAOT
(NativeAOT is used to reduce impact/noise from suspension)

The numbers are averaged GC Gen0 pauses in milliseconds. Lower is better.
On x64, Windows10, AMD 5950X, 16 cores, 32 logical

I see ~ 20% improvement.

==== Before the change:

0.1528027343750001
0.15324121093750026
0.15360449218750016
0.14911621093750022
0.15105566406250034
0.15213964843750036
0.15154882812500012
. . .

=== After the change:

0.12535742187500004
0.12644238281249998
0.12518164062499954
0.12546582031250003
0.12549902343750005
0.12462207031249978
. . .

m_InitialRelPos = other.m_InitialRelPos;
m_pCurrent = other.m_pCurrent;
m_RelPos = other.m_RelPos;
m_current = other.m_current;

@VSadovVSadovDec 19, 2023

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.

On 64bit one native word can act as a "buffer" for quite a few reads when each read takes only a few bits. This change reduces the need for indirect reads from the bitstream and may allow the compiler to enregister the "buffer".


size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

The point of this change is to use a fixed-size shift, which is typically faster than a variable-sized shift.
Same applies to Read( int numBits ) when we read a fixed sized nibble.

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

@VSadovVSadovDec 26, 2023

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 will have to recheck the original codegen, but I think what was happening is that we would do indirect read and then apply a mask that was constructed via a variable shift of 1.
I guess that was because we need the result in a register and do not want to change the bit stream and the ways m_pCurrent and m_RelPos were changing did not allow to hoist/CSE/enregister either the result of the indirect read nor the computed mask.

@VSadovVSadovDec 26, 2023

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.

Interestingly for Zen3 the table gives no difference whatsoever between immediate and CL shift versions.

SHL, SHR, SAR r,i/CL | 1 | 1 | 0.5

yet profiler finds CL shift operations relatively expensive.
I often see that in other code, so I always assumed that varaible shifts are somewhat costly.

Maybe it is not the instruction itself, but the whole sequence of dependent instructions - load 1, load CL, make a mask, do a read, apply the mask, and sampling profiler just attributes most of that to the shift.

The code does get measurably faster after the change. I had a few other changes that did not improve anything so I did not include them, but this one definitely helped.

c++ inlines and interleaves statement parts quite aggressively, so it is a bit hard to see what belongs where, but I see that a few "expensive" CL shifts are gone.
Here is an example of what happened to a loop like:

for(UINT32i=0; i<numSlots; i++)
{
if(m_Reader.ReadOneFast())
numCouldBeLiveSlots++;
}

It is not an example of expensive/hot code, just something that is easier to read and see how codegen changed.

==== original code

00007FF700CE9C4E testr13d,r13d00007FF700CE9C51 je GcInfoDecoder::EnumerateLiveSlots+898h (07FF700CE9C98h) 00007FF700CE9C53 movr8d,r13d00007FF700CE9C56 nop word ptr [rax+rax]00007FF700CE9C60 movecx,r15d00007FF700CE9C63 movrax,r11 ; r11 has `1` in it, assigned outside of the loop00007FF700CE9C66 shlrax,cl ; depends on 2 instructions above00007FF700CE9C69 andrax,qword ptr [rdx] ; depends on instruction above + read (L1 is 3-5 cycles)00007FF700CE9C6C incr15d00007FF700CE9C6F mov dword ptr [rdi+18h],r15d00007FF700CE9C73 cmpr15d,40h00007FF700CE9C77 jne GcInfoDecoder::EnumerateLiveSlots+88Bh (07FF700CE9C8Bh) 00007FF700CE9C79 addrdx,8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF700CE9C7D mov dword ptr [rdi+18h],000007FF700CE9C84 mov qword ptr [rdi+10h],rdx00007FF700CE9C88 xorr15d,r15d00007FF700CE9C8B testrax,rax00007FF700CE9C8E je GcInfoDecoder::EnumerateLiveSlots+893h (07FF700CE9C93h) 00007FF700CE9C90 incr12d00007FF700CE9C93 subr8,r11

vs.

==== new

00007FF688D6A454 testr12d,r12d00007FF688D6A457 je GcInfoDecoder::EnumerateLiveSlots+0C04h (07FF688D6A4B4h) 00007FF688D6A459 movrax,r1100007FF688D6A45C movedx,r12d00007FF688D6A45F nop00007FF688D6A460 movrcx,rax00007FF688D6A463 incr8d ; can run together or even before the previous instruction00007FF688D6A466 shrrax,1 ; this and the next can run at the same time00007FF688D6A469 andecx,1 ; both only depend on "mov rcx,rax"00007FF688D6A46C mov qword ptr [rbx+20h],rax ; we do writes, but we do not need to wait for them00007FF688D6A470 mov dword ptr [rbx+18h],r8d00007FF688D6A474 mov qword ptr [rsp+48h],rax ; not sure what is stored here, in other cases we have just 2 writes00007FF688D6A479 cmpr8d,40h00007FF688D6A47D jne GcInfoDecoder::EnumerateLiveSlots+0BEBh (07FF688D6A49Bh) 00007FF688D6A47F add qword ptr [rbx+10h],8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF688D6A484 movrax,qword ptr [rbx+10h]00007FF688D6A488 xorr8d,r8d00007FF688D6A48B movrax,qword ptr [rax]00007FF688D6A48E mov qword ptr [rbx+20h],rax00007FF688D6A492 mov qword ptr [rsp+48h],rax00007FF688D6A497 mov dword ptr [rbx+18h],r8d00007FF688D6A49B testrcx,rcx00007FF688D6A49E je GcInfoDecoder::EnumerateLiveSlots+0BF3h (07FF688D6A4A3h) 00007FF688D6A4A0 incr13d00007FF688D6A4A3 subrdx,1

bool m_IsInterruptible;
bool m_IsVarArg;
bool m_GenericSecretParamIsMD;
bool m_GenericSecretParamIsMT;

@VSadovVSadovDec 19, 2023

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.

These are derived from masking the header flags. Masking is cheap and we may not even be asked for these, so we can do masking in the accessors.

// Use flag mask to bail out early if we already decoded all the pieces that caller requested
int remainingFlags = flags == DECODE_EVERYTHING ? ~0 : flags;

if (!slimHeader)

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.

Predecoding a fat header could be much more involved compared to slim headers, so let's deal with fat headers in a separate helper.

@VSadov

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@VSadov
VSadov marked this pull request as ready for review December 20, 2023 06:37
@VSadov

Copy link
Copy Markdown
MemberAuthor

I think this is ready for review.

@VSadov
VSadov requested a review from jkotasDecember 25, 2023 04:51
Comment threadsrc/coreclr/inc/gcinfodecoder.h

size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

Comment threadsrc/coreclr/inc/gcinfodecoder.h
if(m_NumSafePoints == 0)
return false;

#if defined(TARGET_AMD64) || defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)

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.

Removal of this ifdef is changing behavior for Linux x86. I guess that it is a bug fix. I do not see a reason why safepoint handling should be different for Linux x86.

cc @gbalykov

@VSadovVSadovDec 28, 2023

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 assumed we can't get here with x86 since we use a different gcinfo encoder.
If that is not the case and we can on Linux, I will put this back.

Overall, I think this -1 hack is trying to solve a problem that does not exist, and it would be better if we could stop doing/undoing this artificial adjustment, but it is not relevant to this PR. I just thought that #if defined is always true here.

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.

@jkotas thanks for sharing this. Currently almost all clr tests fail on linux x86 on main branch, so there're other issues with it that need to be investigated first. We'll probably take a look at this change after that.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thank you!

@VSadov

VSadov commented Dec 28, 2023

Copy link
Copy Markdown
MemberAuthor

It looks like the linux-arm64 Release NativeAOT_Libraries is stuck in a loop repeating the same:

 Done building Helix work items. Work item count: 9
Starting Azure Pipelines Test Run net9.0-linux-Release-arm64-NativeAOT_Release-(Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8
Job e739cf8d-0586-4722-b4a3-61c7ae9aaf97 on (Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8 is completed with 10 finished work items.
Using _DebuggerHosts: chrome
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
BuildTargetFramework: net9.0
TestArchiveTestsRoot: /__w/1/s/artifacts/helix/tests/
TestArchiveRoot: /__w/1/s/artifacts/helix/
TestArchiveRuntimeRoot: /__w/1/s/artifacts/helix/runtime/
TestArchiveRuntimeFile: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Compressing runtime directory
Creating directory /__w/1/s/artifacts/helix/runtime/
Zipping directory "/__w/1/s/artifacts/bin/testhost/net9.0-linux-Release-arm64/" to "/__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip".
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
Build TargetFramework: net9.0
Building Helix work items
Using TestRunNamePrefix: net9.0-linux-Release-arm64-NativeAOT_Release-
Using HelixCorrelationPayload: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Using HelixCommand: ./RunTests.sh --runtime-path "$HELIX_CORRELATION_PAYLOAD"
Using HelixType: test/functional/cli/innerloop/
Using WorkItemArchiveWildCard: /__w/1/s/artifacts/helix/tests/**/*.zip
Using Timeout: 00:45:00
Done building Helix work items. Work item count: 9

https://dev.azure.com/dnceng-public/public/_build/results?buildId=509866&view=logs&j=74bca3e6-08a5-5cef-0121-fe27fe79cc7b&t=317b4f3c-303e-557e-6c7e-66c36fe29909

It is not happening in my newer test run (#85694), so perhaps it is something that got fixed recently.

@VSadov
VSadov merged commit b4ba5da into dotnet:mainDec 28, 2023
@VSadov
VSadov deleted the dec branch December 28, 2023 21:44
@VSadov

Copy link
Copy Markdown
MemberAuthor

Thanks!!!

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 4, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrtenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@VSadov@jkotas@davidwrighton@gbalykov
, '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

A few optimizations for the gcinfodecoder construction - #96150

Merged
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec
Dec 28, 2023
Merged

A few optimizations for the gcinfodecoder construction#96150
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec

Conversation

@VSadov

@VSadovVSadov commented Dec 18, 2023

Copy link
Copy Markdown
Member

Constructing gcinfodecoder performs some initial decoding. While the time spent in initial decoding is typically less than the time spent enumerating live slots, it is not insignificant. There are some opportunities to do initial decoding a bit cheaper.

Comment threadsrc/coreclr/inc/gcinfotypes.h Outdated
DWORD lzcountCeil;
_BitScanReverse(&lzcountCeil, (unsigned long)x);
#else // _MSC_VER
UINT32 lzcountCeil = (UINT32)__builtin_clz((unsigned int)x);

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.

I believe __builtin_clz already encodes the subtract from BITS_PER_SIZE_T within the intrinsic function unlike _BitScanReverse

@VSadovVSadovDec 19, 2023

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.

right, I've just realized that even though lzcnt is encoded similarly to bsr on x64, the result is an offset from different ends.

@VSadov

VSadov commented Dec 19, 2023

Copy link
Copy Markdown
MemberAuthor

To measure the impact I use the following microbenchmark compiled with NativeAOT
(NativeAOT is used to reduce impact/noise from suspension)

The numbers are averaged GC Gen0 pauses in milliseconds. Lower is better.
On x64, Windows10, AMD 5950X, 16 cores, 32 logical

I see ~ 20% improvement.

==== Before the change:

0.1528027343750001
0.15324121093750026
0.15360449218750016
0.14911621093750022
0.15105566406250034
0.15213964843750036
0.15154882812500012
. . .

=== After the change:

0.12535742187500004
0.12644238281249998
0.12518164062499954
0.12546582031250003
0.12549902343750005
0.12462207031249978
. . .

m_InitialRelPos = other.m_InitialRelPos;
m_pCurrent = other.m_pCurrent;
m_RelPos = other.m_RelPos;
m_current = other.m_current;

@VSadovVSadovDec 19, 2023

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.

On 64bit one native word can act as a "buffer" for quite a few reads when each read takes only a few bits. This change reduces the need for indirect reads from the bitstream and may allow the compiler to enregister the "buffer".


size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

The point of this change is to use a fixed-size shift, which is typically faster than a variable-sized shift.
Same applies to Read( int numBits ) when we read a fixed sized nibble.

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

@VSadovVSadovDec 26, 2023

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 will have to recheck the original codegen, but I think what was happening is that we would do indirect read and then apply a mask that was constructed via a variable shift of 1.
I guess that was because we need the result in a register and do not want to change the bit stream and the ways m_pCurrent and m_RelPos were changing did not allow to hoist/CSE/enregister either the result of the indirect read nor the computed mask.

@VSadovVSadovDec 26, 2023

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.

Interestingly for Zen3 the table gives no difference whatsoever between immediate and CL shift versions.

SHL, SHR, SAR r,i/CL | 1 | 1 | 0.5

yet profiler finds CL shift operations relatively expensive.
I often see that in other code, so I always assumed that varaible shifts are somewhat costly.

Maybe it is not the instruction itself, but the whole sequence of dependent instructions - load 1, load CL, make a mask, do a read, apply the mask, and sampling profiler just attributes most of that to the shift.

The code does get measurably faster after the change. I had a few other changes that did not improve anything so I did not include them, but this one definitely helped.

c++ inlines and interleaves statement parts quite aggressively, so it is a bit hard to see what belongs where, but I see that a few "expensive" CL shifts are gone.
Here is an example of what happened to a loop like:

for(UINT32i=0; i<numSlots; i++)
{
if(m_Reader.ReadOneFast())
numCouldBeLiveSlots++;
}

It is not an example of expensive/hot code, just something that is easier to read and see how codegen changed.

==== original code

00007FF700CE9C4E testr13d,r13d00007FF700CE9C51 je GcInfoDecoder::EnumerateLiveSlots+898h (07FF700CE9C98h) 00007FF700CE9C53 movr8d,r13d00007FF700CE9C56 nop word ptr [rax+rax]00007FF700CE9C60 movecx,r15d00007FF700CE9C63 movrax,r11 ; r11 has `1` in it, assigned outside of the loop00007FF700CE9C66 shlrax,cl ; depends on 2 instructions above00007FF700CE9C69 andrax,qword ptr [rdx] ; depends on instruction above + read (L1 is 3-5 cycles)00007FF700CE9C6C incr15d00007FF700CE9C6F mov dword ptr [rdi+18h],r15d00007FF700CE9C73 cmpr15d,40h00007FF700CE9C77 jne GcInfoDecoder::EnumerateLiveSlots+88Bh (07FF700CE9C8Bh) 00007FF700CE9C79 addrdx,8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF700CE9C7D mov dword ptr [rdi+18h],000007FF700CE9C84 mov qword ptr [rdi+10h],rdx00007FF700CE9C88 xorr15d,r15d00007FF700CE9C8B testrax,rax00007FF700CE9C8E je GcInfoDecoder::EnumerateLiveSlots+893h (07FF700CE9C93h) 00007FF700CE9C90 incr12d00007FF700CE9C93 subr8,r11

vs.

==== new

00007FF688D6A454 testr12d,r12d00007FF688D6A457 je GcInfoDecoder::EnumerateLiveSlots+0C04h (07FF688D6A4B4h) 00007FF688D6A459 movrax,r1100007FF688D6A45C movedx,r12d00007FF688D6A45F nop00007FF688D6A460 movrcx,rax00007FF688D6A463 incr8d ; can run together or even before the previous instruction00007FF688D6A466 shrrax,1 ; this and the next can run at the same time00007FF688D6A469 andecx,1 ; both only depend on "mov rcx,rax"00007FF688D6A46C mov qword ptr [rbx+20h],rax ; we do writes, but we do not need to wait for them00007FF688D6A470 mov dword ptr [rbx+18h],r8d00007FF688D6A474 mov qword ptr [rsp+48h],rax ; not sure what is stored here, in other cases we have just 2 writes00007FF688D6A479 cmpr8d,40h00007FF688D6A47D jne GcInfoDecoder::EnumerateLiveSlots+0BEBh (07FF688D6A49Bh) 00007FF688D6A47F add qword ptr [rbx+10h],8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF688D6A484 movrax,qword ptr [rbx+10h]00007FF688D6A488 xorr8d,r8d00007FF688D6A48B movrax,qword ptr [rax]00007FF688D6A48E mov qword ptr [rbx+20h],rax00007FF688D6A492 mov qword ptr [rsp+48h],rax00007FF688D6A497 mov dword ptr [rbx+18h],r8d00007FF688D6A49B testrcx,rcx00007FF688D6A49E je GcInfoDecoder::EnumerateLiveSlots+0BF3h (07FF688D6A4A3h) 00007FF688D6A4A0 incr13d00007FF688D6A4A3 subrdx,1

bool m_IsInterruptible;
bool m_IsVarArg;
bool m_GenericSecretParamIsMD;
bool m_GenericSecretParamIsMT;

@VSadovVSadovDec 19, 2023

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.

These are derived from masking the header flags. Masking is cheap and we may not even be asked for these, so we can do masking in the accessors.

// Use flag mask to bail out early if we already decoded all the pieces that caller requested
int remainingFlags = flags == DECODE_EVERYTHING ? ~0 : flags;

if (!slimHeader)

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.

Predecoding a fat header could be much more involved compared to slim headers, so let's deal with fat headers in a separate helper.

@VSadov

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@VSadov
VSadov marked this pull request as ready for review December 20, 2023 06:37
@VSadov

Copy link
Copy Markdown
MemberAuthor

I think this is ready for review.

@VSadov
VSadov requested a review from jkotasDecember 25, 2023 04:51
Comment threadsrc/coreclr/inc/gcinfodecoder.h

size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

Comment threadsrc/coreclr/inc/gcinfodecoder.h
if(m_NumSafePoints == 0)
return false;

#if defined(TARGET_AMD64) || defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)

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.

Removal of this ifdef is changing behavior for Linux x86. I guess that it is a bug fix. I do not see a reason why safepoint handling should be different for Linux x86.

cc @gbalykov

@VSadovVSadovDec 28, 2023

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 assumed we can't get here with x86 since we use a different gcinfo encoder.
If that is not the case and we can on Linux, I will put this back.

Overall, I think this -1 hack is trying to solve a problem that does not exist, and it would be better if we could stop doing/undoing this artificial adjustment, but it is not relevant to this PR. I just thought that #if defined is always true here.

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.

@jkotas thanks for sharing this. Currently almost all clr tests fail on linux x86 on main branch, so there're other issues with it that need to be investigated first. We'll probably take a look at this change after that.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thank you!

@VSadov

VSadov commented Dec 28, 2023

Copy link
Copy Markdown
MemberAuthor

It looks like the linux-arm64 Release NativeAOT_Libraries is stuck in a loop repeating the same:

 Done building Helix work items. Work item count: 9
Starting Azure Pipelines Test Run net9.0-linux-Release-arm64-NativeAOT_Release-(Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8
Job e739cf8d-0586-4722-b4a3-61c7ae9aaf97 on (Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8 is completed with 10 finished work items.
Using _DebuggerHosts: chrome
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
BuildTargetFramework: net9.0
TestArchiveTestsRoot: /__w/1/s/artifacts/helix/tests/
TestArchiveRoot: /__w/1/s/artifacts/helix/
TestArchiveRuntimeRoot: /__w/1/s/artifacts/helix/runtime/
TestArchiveRuntimeFile: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Compressing runtime directory
Creating directory /__w/1/s/artifacts/helix/runtime/
Zipping directory "/__w/1/s/artifacts/bin/testhost/net9.0-linux-Release-arm64/" to "/__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip".
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
Build TargetFramework: net9.0
Building Helix work items
Using TestRunNamePrefix: net9.0-linux-Release-arm64-NativeAOT_Release-
Using HelixCorrelationPayload: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Using HelixCommand: ./RunTests.sh --runtime-path "$HELIX_CORRELATION_PAYLOAD"
Using HelixType: test/functional/cli/innerloop/
Using WorkItemArchiveWildCard: /__w/1/s/artifacts/helix/tests/**/*.zip
Using Timeout: 00:45:00
Done building Helix work items. Work item count: 9

https://dev.azure.com/dnceng-public/public/_build/results?buildId=509866&view=logs&j=74bca3e6-08a5-5cef-0121-fe27fe79cc7b&t=317b4f3c-303e-557e-6c7e-66c36fe29909

It is not happening in my newer test run (#85694), so perhaps it is something that got fixed recently.

@VSadov
VSadov merged commit b4ba5da into dotnet:mainDec 28, 2023
@VSadov
VSadov deleted the dec branch December 28, 2023 21:44
@VSadov

Copy link
Copy Markdown
MemberAuthor

Thanks!!!

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 4, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrtenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@VSadov@jkotas@davidwrighton@gbalykov
, '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

A few optimizations for the gcinfodecoder construction - #96150

Merged
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec
Dec 28, 2023
Merged

A few optimizations for the gcinfodecoder construction#96150
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec

Conversation

@VSadov

@VSadovVSadov commented Dec 18, 2023

Copy link
Copy Markdown
Member

Constructing gcinfodecoder performs some initial decoding. While the time spent in initial decoding is typically less than the time spent enumerating live slots, it is not insignificant. There are some opportunities to do initial decoding a bit cheaper.

Comment threadsrc/coreclr/inc/gcinfotypes.h Outdated
DWORD lzcountCeil;
_BitScanReverse(&lzcountCeil, (unsigned long)x);
#else // _MSC_VER
UINT32 lzcountCeil = (UINT32)__builtin_clz((unsigned int)x);

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.

I believe __builtin_clz already encodes the subtract from BITS_PER_SIZE_T within the intrinsic function unlike _BitScanReverse

@VSadovVSadovDec 19, 2023

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.

right, I've just realized that even though lzcnt is encoded similarly to bsr on x64, the result is an offset from different ends.

@VSadov

VSadov commented Dec 19, 2023

Copy link
Copy Markdown
MemberAuthor

To measure the impact I use the following microbenchmark compiled with NativeAOT
(NativeAOT is used to reduce impact/noise from suspension)

The numbers are averaged GC Gen0 pauses in milliseconds. Lower is better.
On x64, Windows10, AMD 5950X, 16 cores, 32 logical

I see ~ 20% improvement.

==== Before the change:

0.1528027343750001
0.15324121093750026
0.15360449218750016
0.14911621093750022
0.15105566406250034
0.15213964843750036
0.15154882812500012
. . .

=== After the change:

0.12535742187500004
0.12644238281249998
0.12518164062499954
0.12546582031250003
0.12549902343750005
0.12462207031249978
. . .

m_InitialRelPos = other.m_InitialRelPos;
m_pCurrent = other.m_pCurrent;
m_RelPos = other.m_RelPos;
m_current = other.m_current;

@VSadovVSadovDec 19, 2023

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.

On 64bit one native word can act as a "buffer" for quite a few reads when each read takes only a few bits. This change reduces the need for indirect reads from the bitstream and may allow the compiler to enregister the "buffer".


size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

The point of this change is to use a fixed-size shift, which is typically faster than a variable-sized shift.
Same applies to Read( int numBits ) when we read a fixed sized nibble.

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

@VSadovVSadovDec 26, 2023

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 will have to recheck the original codegen, but I think what was happening is that we would do indirect read and then apply a mask that was constructed via a variable shift of 1.
I guess that was because we need the result in a register and do not want to change the bit stream and the ways m_pCurrent and m_RelPos were changing did not allow to hoist/CSE/enregister either the result of the indirect read nor the computed mask.

@VSadovVSadovDec 26, 2023

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.

Interestingly for Zen3 the table gives no difference whatsoever between immediate and CL shift versions.

SHL, SHR, SAR r,i/CL | 1 | 1 | 0.5

yet profiler finds CL shift operations relatively expensive.
I often see that in other code, so I always assumed that varaible shifts are somewhat costly.

Maybe it is not the instruction itself, but the whole sequence of dependent instructions - load 1, load CL, make a mask, do a read, apply the mask, and sampling profiler just attributes most of that to the shift.

The code does get measurably faster after the change. I had a few other changes that did not improve anything so I did not include them, but this one definitely helped.

c++ inlines and interleaves statement parts quite aggressively, so it is a bit hard to see what belongs where, but I see that a few "expensive" CL shifts are gone.
Here is an example of what happened to a loop like:

for(UINT32i=0; i<numSlots; i++)
{
if(m_Reader.ReadOneFast())
numCouldBeLiveSlots++;
}

It is not an example of expensive/hot code, just something that is easier to read and see how codegen changed.

==== original code

00007FF700CE9C4E testr13d,r13d00007FF700CE9C51 je GcInfoDecoder::EnumerateLiveSlots+898h (07FF700CE9C98h) 00007FF700CE9C53 movr8d,r13d00007FF700CE9C56 nop word ptr [rax+rax]00007FF700CE9C60 movecx,r15d00007FF700CE9C63 movrax,r11 ; r11 has `1` in it, assigned outside of the loop00007FF700CE9C66 shlrax,cl ; depends on 2 instructions above00007FF700CE9C69 andrax,qword ptr [rdx] ; depends on instruction above + read (L1 is 3-5 cycles)00007FF700CE9C6C incr15d00007FF700CE9C6F mov dword ptr [rdi+18h],r15d00007FF700CE9C73 cmpr15d,40h00007FF700CE9C77 jne GcInfoDecoder::EnumerateLiveSlots+88Bh (07FF700CE9C8Bh) 00007FF700CE9C79 addrdx,8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF700CE9C7D mov dword ptr [rdi+18h],000007FF700CE9C84 mov qword ptr [rdi+10h],rdx00007FF700CE9C88 xorr15d,r15d00007FF700CE9C8B testrax,rax00007FF700CE9C8E je GcInfoDecoder::EnumerateLiveSlots+893h (07FF700CE9C93h) 00007FF700CE9C90 incr12d00007FF700CE9C93 subr8,r11

vs.

==== new

00007FF688D6A454 testr12d,r12d00007FF688D6A457 je GcInfoDecoder::EnumerateLiveSlots+0C04h (07FF688D6A4B4h) 00007FF688D6A459 movrax,r1100007FF688D6A45C movedx,r12d00007FF688D6A45F nop00007FF688D6A460 movrcx,rax00007FF688D6A463 incr8d ; can run together or even before the previous instruction00007FF688D6A466 shrrax,1 ; this and the next can run at the same time00007FF688D6A469 andecx,1 ; both only depend on "mov rcx,rax"00007FF688D6A46C mov qword ptr [rbx+20h],rax ; we do writes, but we do not need to wait for them00007FF688D6A470 mov dword ptr [rbx+18h],r8d00007FF688D6A474 mov qword ptr [rsp+48h],rax ; not sure what is stored here, in other cases we have just 2 writes00007FF688D6A479 cmpr8d,40h00007FF688D6A47D jne GcInfoDecoder::EnumerateLiveSlots+0BEBh (07FF688D6A49Bh) 00007FF688D6A47F add qword ptr [rbx+10h],8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF688D6A484 movrax,qword ptr [rbx+10h]00007FF688D6A488 xorr8d,r8d00007FF688D6A48B movrax,qword ptr [rax]00007FF688D6A48E mov qword ptr [rbx+20h],rax00007FF688D6A492 mov qword ptr [rsp+48h],rax00007FF688D6A497 mov dword ptr [rbx+18h],r8d00007FF688D6A49B testrcx,rcx00007FF688D6A49E je GcInfoDecoder::EnumerateLiveSlots+0BF3h (07FF688D6A4A3h) 00007FF688D6A4A0 incr13d00007FF688D6A4A3 subrdx,1

bool m_IsInterruptible;
bool m_IsVarArg;
bool m_GenericSecretParamIsMD;
bool m_GenericSecretParamIsMT;

@VSadovVSadovDec 19, 2023

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.

These are derived from masking the header flags. Masking is cheap and we may not even be asked for these, so we can do masking in the accessors.

// Use flag mask to bail out early if we already decoded all the pieces that caller requested
int remainingFlags = flags == DECODE_EVERYTHING ? ~0 : flags;

if (!slimHeader)

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.

Predecoding a fat header could be much more involved compared to slim headers, so let's deal with fat headers in a separate helper.

@VSadov

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@VSadov
VSadov marked this pull request as ready for review December 20, 2023 06:37
@VSadov

Copy link
Copy Markdown
MemberAuthor

I think this is ready for review.

@VSadov
VSadov requested a review from jkotasDecember 25, 2023 04:51
Comment threadsrc/coreclr/inc/gcinfodecoder.h

size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

Comment threadsrc/coreclr/inc/gcinfodecoder.h
if(m_NumSafePoints == 0)
return false;

#if defined(TARGET_AMD64) || defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)

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.

Removal of this ifdef is changing behavior for Linux x86. I guess that it is a bug fix. I do not see a reason why safepoint handling should be different for Linux x86.

cc @gbalykov

@VSadovVSadovDec 28, 2023

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 assumed we can't get here with x86 since we use a different gcinfo encoder.
If that is not the case and we can on Linux, I will put this back.

Overall, I think this -1 hack is trying to solve a problem that does not exist, and it would be better if we could stop doing/undoing this artificial adjustment, but it is not relevant to this PR. I just thought that #if defined is always true here.

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.

@jkotas thanks for sharing this. Currently almost all clr tests fail on linux x86 on main branch, so there're other issues with it that need to be investigated first. We'll probably take a look at this change after that.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thank you!

@VSadov

VSadov commented Dec 28, 2023

Copy link
Copy Markdown
MemberAuthor

It looks like the linux-arm64 Release NativeAOT_Libraries is stuck in a loop repeating the same:

 Done building Helix work items. Work item count: 9
Starting Azure Pipelines Test Run net9.0-linux-Release-arm64-NativeAOT_Release-(Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8
Job e739cf8d-0586-4722-b4a3-61c7ae9aaf97 on (Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8 is completed with 10 finished work items.
Using _DebuggerHosts: chrome
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
BuildTargetFramework: net9.0
TestArchiveTestsRoot: /__w/1/s/artifacts/helix/tests/
TestArchiveRoot: /__w/1/s/artifacts/helix/
TestArchiveRuntimeRoot: /__w/1/s/artifacts/helix/runtime/
TestArchiveRuntimeFile: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Compressing runtime directory
Creating directory /__w/1/s/artifacts/helix/runtime/
Zipping directory "/__w/1/s/artifacts/bin/testhost/net9.0-linux-Release-arm64/" to "/__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip".
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
Build TargetFramework: net9.0
Building Helix work items
Using TestRunNamePrefix: net9.0-linux-Release-arm64-NativeAOT_Release-
Using HelixCorrelationPayload: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Using HelixCommand: ./RunTests.sh --runtime-path "$HELIX_CORRELATION_PAYLOAD"
Using HelixType: test/functional/cli/innerloop/
Using WorkItemArchiveWildCard: /__w/1/s/artifacts/helix/tests/**/*.zip
Using Timeout: 00:45:00
Done building Helix work items. Work item count: 9

https://dev.azure.com/dnceng-public/public/_build/results?buildId=509866&view=logs&j=74bca3e6-08a5-5cef-0121-fe27fe79cc7b&t=317b4f3c-303e-557e-6c7e-66c36fe29909

It is not happening in my newer test run (#85694), so perhaps it is something that got fixed recently.

@VSadov
VSadov merged commit b4ba5da into dotnet:mainDec 28, 2023
@VSadov
VSadov deleted the dec branch December 28, 2023 21:44
@VSadov

Copy link
Copy Markdown
MemberAuthor

Thanks!!!

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 4, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrtenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@VSadov@jkotas@davidwrighton@gbalykov
, '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

A few optimizations for the gcinfodecoder construction - #96150

Merged
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec
Dec 28, 2023
Merged

A few optimizations for the gcinfodecoder construction#96150
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec

Conversation

@VSadov

@VSadovVSadov commented Dec 18, 2023

Copy link
Copy Markdown
Member

Constructing gcinfodecoder performs some initial decoding. While the time spent in initial decoding is typically less than the time spent enumerating live slots, it is not insignificant. There are some opportunities to do initial decoding a bit cheaper.

Comment threadsrc/coreclr/inc/gcinfotypes.h Outdated
DWORD lzcountCeil;
_BitScanReverse(&lzcountCeil, (unsigned long)x);
#else // _MSC_VER
UINT32 lzcountCeil = (UINT32)__builtin_clz((unsigned int)x);

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.

I believe __builtin_clz already encodes the subtract from BITS_PER_SIZE_T within the intrinsic function unlike _BitScanReverse

@VSadovVSadovDec 19, 2023

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.

right, I've just realized that even though lzcnt is encoded similarly to bsr on x64, the result is an offset from different ends.

@VSadov

VSadov commented Dec 19, 2023

Copy link
Copy Markdown
MemberAuthor

To measure the impact I use the following microbenchmark compiled with NativeAOT
(NativeAOT is used to reduce impact/noise from suspension)

The numbers are averaged GC Gen0 pauses in milliseconds. Lower is better.
On x64, Windows10, AMD 5950X, 16 cores, 32 logical

I see ~ 20% improvement.

==== Before the change:

0.1528027343750001
0.15324121093750026
0.15360449218750016
0.14911621093750022
0.15105566406250034
0.15213964843750036
0.15154882812500012
. . .

=== After the change:

0.12535742187500004
0.12644238281249998
0.12518164062499954
0.12546582031250003
0.12549902343750005
0.12462207031249978
. . .

m_InitialRelPos = other.m_InitialRelPos;
m_pCurrent = other.m_pCurrent;
m_RelPos = other.m_RelPos;
m_current = other.m_current;

@VSadovVSadovDec 19, 2023

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.

On 64bit one native word can act as a "buffer" for quite a few reads when each read takes only a few bits. This change reduces the need for indirect reads from the bitstream and may allow the compiler to enregister the "buffer".


size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

The point of this change is to use a fixed-size shift, which is typically faster than a variable-sized shift.
Same applies to Read( int numBits ) when we read a fixed sized nibble.

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

@VSadovVSadovDec 26, 2023

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 will have to recheck the original codegen, but I think what was happening is that we would do indirect read and then apply a mask that was constructed via a variable shift of 1.
I guess that was because we need the result in a register and do not want to change the bit stream and the ways m_pCurrent and m_RelPos were changing did not allow to hoist/CSE/enregister either the result of the indirect read nor the computed mask.

@VSadovVSadovDec 26, 2023

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.

Interestingly for Zen3 the table gives no difference whatsoever between immediate and CL shift versions.

SHL, SHR, SAR r,i/CL | 1 | 1 | 0.5

yet profiler finds CL shift operations relatively expensive.
I often see that in other code, so I always assumed that varaible shifts are somewhat costly.

Maybe it is not the instruction itself, but the whole sequence of dependent instructions - load 1, load CL, make a mask, do a read, apply the mask, and sampling profiler just attributes most of that to the shift.

The code does get measurably faster after the change. I had a few other changes that did not improve anything so I did not include them, but this one definitely helped.

c++ inlines and interleaves statement parts quite aggressively, so it is a bit hard to see what belongs where, but I see that a few "expensive" CL shifts are gone.
Here is an example of what happened to a loop like:

for(UINT32i=0; i<numSlots; i++)
{
if(m_Reader.ReadOneFast())
numCouldBeLiveSlots++;
}

It is not an example of expensive/hot code, just something that is easier to read and see how codegen changed.

==== original code

00007FF700CE9C4E testr13d,r13d00007FF700CE9C51 je GcInfoDecoder::EnumerateLiveSlots+898h (07FF700CE9C98h) 00007FF700CE9C53 movr8d,r13d00007FF700CE9C56 nop word ptr [rax+rax]00007FF700CE9C60 movecx,r15d00007FF700CE9C63 movrax,r11 ; r11 has `1` in it, assigned outside of the loop00007FF700CE9C66 shlrax,cl ; depends on 2 instructions above00007FF700CE9C69 andrax,qword ptr [rdx] ; depends on instruction above + read (L1 is 3-5 cycles)00007FF700CE9C6C incr15d00007FF700CE9C6F mov dword ptr [rdi+18h],r15d00007FF700CE9C73 cmpr15d,40h00007FF700CE9C77 jne GcInfoDecoder::EnumerateLiveSlots+88Bh (07FF700CE9C8Bh) 00007FF700CE9C79 addrdx,8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF700CE9C7D mov dword ptr [rdi+18h],000007FF700CE9C84 mov qword ptr [rdi+10h],rdx00007FF700CE9C88 xorr15d,r15d00007FF700CE9C8B testrax,rax00007FF700CE9C8E je GcInfoDecoder::EnumerateLiveSlots+893h (07FF700CE9C93h) 00007FF700CE9C90 incr12d00007FF700CE9C93 subr8,r11

vs.

==== new

00007FF688D6A454 testr12d,r12d00007FF688D6A457 je GcInfoDecoder::EnumerateLiveSlots+0C04h (07FF688D6A4B4h) 00007FF688D6A459 movrax,r1100007FF688D6A45C movedx,r12d00007FF688D6A45F nop00007FF688D6A460 movrcx,rax00007FF688D6A463 incr8d ; can run together or even before the previous instruction00007FF688D6A466 shrrax,1 ; this and the next can run at the same time00007FF688D6A469 andecx,1 ; both only depend on "mov rcx,rax"00007FF688D6A46C mov qword ptr [rbx+20h],rax ; we do writes, but we do not need to wait for them00007FF688D6A470 mov dword ptr [rbx+18h],r8d00007FF688D6A474 mov qword ptr [rsp+48h],rax ; not sure what is stored here, in other cases we have just 2 writes00007FF688D6A479 cmpr8d,40h00007FF688D6A47D jne GcInfoDecoder::EnumerateLiveSlots+0BEBh (07FF688D6A49Bh) 00007FF688D6A47F add qword ptr [rbx+10h],8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF688D6A484 movrax,qword ptr [rbx+10h]00007FF688D6A488 xorr8d,r8d00007FF688D6A48B movrax,qword ptr [rax]00007FF688D6A48E mov qword ptr [rbx+20h],rax00007FF688D6A492 mov qword ptr [rsp+48h],rax00007FF688D6A497 mov dword ptr [rbx+18h],r8d00007FF688D6A49B testrcx,rcx00007FF688D6A49E je GcInfoDecoder::EnumerateLiveSlots+0BF3h (07FF688D6A4A3h) 00007FF688D6A4A0 incr13d00007FF688D6A4A3 subrdx,1

bool m_IsInterruptible;
bool m_IsVarArg;
bool m_GenericSecretParamIsMD;
bool m_GenericSecretParamIsMT;

@VSadovVSadovDec 19, 2023

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.

These are derived from masking the header flags. Masking is cheap and we may not even be asked for these, so we can do masking in the accessors.

// Use flag mask to bail out early if we already decoded all the pieces that caller requested
int remainingFlags = flags == DECODE_EVERYTHING ? ~0 : flags;

if (!slimHeader)

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.

Predecoding a fat header could be much more involved compared to slim headers, so let's deal with fat headers in a separate helper.

@VSadov

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@VSadov
VSadov marked this pull request as ready for review December 20, 2023 06:37
@VSadov

Copy link
Copy Markdown
MemberAuthor

I think this is ready for review.

@VSadov
VSadov requested a review from jkotasDecember 25, 2023 04:51
Comment threadsrc/coreclr/inc/gcinfodecoder.h

size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

Comment threadsrc/coreclr/inc/gcinfodecoder.h
if(m_NumSafePoints == 0)
return false;

#if defined(TARGET_AMD64) || defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)

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.

Removal of this ifdef is changing behavior for Linux x86. I guess that it is a bug fix. I do not see a reason why safepoint handling should be different for Linux x86.

cc @gbalykov

@VSadovVSadovDec 28, 2023

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 assumed we can't get here with x86 since we use a different gcinfo encoder.
If that is not the case and we can on Linux, I will put this back.

Overall, I think this -1 hack is trying to solve a problem that does not exist, and it would be better if we could stop doing/undoing this artificial adjustment, but it is not relevant to this PR. I just thought that #if defined is always true here.

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.

@jkotas thanks for sharing this. Currently almost all clr tests fail on linux x86 on main branch, so there're other issues with it that need to be investigated first. We'll probably take a look at this change after that.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thank you!

@VSadov

VSadov commented Dec 28, 2023

Copy link
Copy Markdown
MemberAuthor

It looks like the linux-arm64 Release NativeAOT_Libraries is stuck in a loop repeating the same:

 Done building Helix work items. Work item count: 9
Starting Azure Pipelines Test Run net9.0-linux-Release-arm64-NativeAOT_Release-(Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8
Job e739cf8d-0586-4722-b4a3-61c7ae9aaf97 on (Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8 is completed with 10 finished work items.
Using _DebuggerHosts: chrome
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
BuildTargetFramework: net9.0
TestArchiveTestsRoot: /__w/1/s/artifacts/helix/tests/
TestArchiveRoot: /__w/1/s/artifacts/helix/
TestArchiveRuntimeRoot: /__w/1/s/artifacts/helix/runtime/
TestArchiveRuntimeFile: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Compressing runtime directory
Creating directory /__w/1/s/artifacts/helix/runtime/
Zipping directory "/__w/1/s/artifacts/bin/testhost/net9.0-linux-Release-arm64/" to "/__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip".
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
Build TargetFramework: net9.0
Building Helix work items
Using TestRunNamePrefix: net9.0-linux-Release-arm64-NativeAOT_Release-
Using HelixCorrelationPayload: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Using HelixCommand: ./RunTests.sh --runtime-path "$HELIX_CORRELATION_PAYLOAD"
Using HelixType: test/functional/cli/innerloop/
Using WorkItemArchiveWildCard: /__w/1/s/artifacts/helix/tests/**/*.zip
Using Timeout: 00:45:00
Done building Helix work items. Work item count: 9

https://dev.azure.com/dnceng-public/public/_build/results?buildId=509866&view=logs&j=74bca3e6-08a5-5cef-0121-fe27fe79cc7b&t=317b4f3c-303e-557e-6c7e-66c36fe29909

It is not happening in my newer test run (#85694), so perhaps it is something that got fixed recently.

@VSadov
VSadov merged commit b4ba5da into dotnet:mainDec 28, 2023
@VSadov
VSadov deleted the dec branch December 28, 2023 21:44
@VSadov

Copy link
Copy Markdown
MemberAuthor

Thanks!!!

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 4, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrtenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@VSadov@jkotas@davidwrighton@gbalykov
, '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

A few optimizations for the gcinfodecoder construction - #96150

Merged
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec
Dec 28, 2023
Merged

A few optimizations for the gcinfodecoder construction#96150
VSadov merged 13 commits into
dotnet:mainfrom
VSadov:dec

Conversation

@VSadov

@VSadovVSadov commented Dec 18, 2023

Copy link
Copy Markdown
Member

Constructing gcinfodecoder performs some initial decoding. While the time spent in initial decoding is typically less than the time spent enumerating live slots, it is not insignificant. There are some opportunities to do initial decoding a bit cheaper.

Comment threadsrc/coreclr/inc/gcinfotypes.h Outdated
DWORD lzcountCeil;
_BitScanReverse(&lzcountCeil, (unsigned long)x);
#else // _MSC_VER
UINT32 lzcountCeil = (UINT32)__builtin_clz((unsigned int)x);

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.

I believe __builtin_clz already encodes the subtract from BITS_PER_SIZE_T within the intrinsic function unlike _BitScanReverse

@VSadovVSadovDec 19, 2023

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.

right, I've just realized that even though lzcnt is encoded similarly to bsr on x64, the result is an offset from different ends.

@VSadov

VSadov commented Dec 19, 2023

Copy link
Copy Markdown
MemberAuthor

To measure the impact I use the following microbenchmark compiled with NativeAOT
(NativeAOT is used to reduce impact/noise from suspension)

The numbers are averaged GC Gen0 pauses in milliseconds. Lower is better.
On x64, Windows10, AMD 5950X, 16 cores, 32 logical

I see ~ 20% improvement.

==== Before the change:

0.1528027343750001
0.15324121093750026
0.15360449218750016
0.14911621093750022
0.15105566406250034
0.15213964843750036
0.15154882812500012
. . .

=== After the change:

0.12535742187500004
0.12644238281249998
0.12518164062499954
0.12546582031250003
0.12549902343750005
0.12462207031249978
. . .

m_InitialRelPos = other.m_InitialRelPos;
m_pCurrent = other.m_pCurrent;
m_RelPos = other.m_RelPos;
m_current = other.m_current;

@VSadovVSadovDec 19, 2023

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.

On 64bit one native word can act as a "buffer" for quite a few reads when each read takes only a few bits. This change reduces the need for indirect reads from the bitstream and may allow the compiler to enregister the "buffer".


size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

The point of this change is to use a fixed-size shift, which is typically faster than a variable-sized shift.
Same applies to Read( int numBits ) when we read a fixed sized nibble.

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

@VSadovVSadovDec 26, 2023

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 will have to recheck the original codegen, but I think what was happening is that we would do indirect read and then apply a mask that was constructed via a variable shift of 1.
I guess that was because we need the result in a register and do not want to change the bit stream and the ways m_pCurrent and m_RelPos were changing did not allow to hoist/CSE/enregister either the result of the indirect read nor the computed mask.

@VSadovVSadovDec 26, 2023

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.

Interestingly for Zen3 the table gives no difference whatsoever between immediate and CL shift versions.

SHL, SHR, SAR r,i/CL | 1 | 1 | 0.5

yet profiler finds CL shift operations relatively expensive.
I often see that in other code, so I always assumed that varaible shifts are somewhat costly.

Maybe it is not the instruction itself, but the whole sequence of dependent instructions - load 1, load CL, make a mask, do a read, apply the mask, and sampling profiler just attributes most of that to the shift.

The code does get measurably faster after the change. I had a few other changes that did not improve anything so I did not include them, but this one definitely helped.

c++ inlines and interleaves statement parts quite aggressively, so it is a bit hard to see what belongs where, but I see that a few "expensive" CL shifts are gone.
Here is an example of what happened to a loop like:

for(UINT32i=0; i<numSlots; i++)
{
if(m_Reader.ReadOneFast())
numCouldBeLiveSlots++;
}

It is not an example of expensive/hot code, just something that is easier to read and see how codegen changed.

==== original code

00007FF700CE9C4E testr13d,r13d00007FF700CE9C51 je GcInfoDecoder::EnumerateLiveSlots+898h (07FF700CE9C98h) 00007FF700CE9C53 movr8d,r13d00007FF700CE9C56 nop word ptr [rax+rax]00007FF700CE9C60 movecx,r15d00007FF700CE9C63 movrax,r11 ; r11 has `1` in it, assigned outside of the loop00007FF700CE9C66 shlrax,cl ; depends on 2 instructions above00007FF700CE9C69 andrax,qword ptr [rdx] ; depends on instruction above + read (L1 is 3-5 cycles)00007FF700CE9C6C incr15d00007FF700CE9C6F mov dword ptr [rdi+18h],r15d00007FF700CE9C73 cmpr15d,40h00007FF700CE9C77 jne GcInfoDecoder::EnumerateLiveSlots+88Bh (07FF700CE9C8Bh) 00007FF700CE9C79 addrdx,8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF700CE9C7D mov dword ptr [rdi+18h],000007FF700CE9C84 mov qword ptr [rdi+10h],rdx00007FF700CE9C88 xorr15d,r15d00007FF700CE9C8B testrax,rax00007FF700CE9C8E je GcInfoDecoder::EnumerateLiveSlots+893h (07FF700CE9C93h) 00007FF700CE9C90 incr12d00007FF700CE9C93 subr8,r11

vs.

==== new

00007FF688D6A454 testr12d,r12d00007FF688D6A457 je GcInfoDecoder::EnumerateLiveSlots+0C04h (07FF688D6A4B4h) 00007FF688D6A459 movrax,r1100007FF688D6A45C movedx,r12d00007FF688D6A45F nop00007FF688D6A460 movrcx,rax00007FF688D6A463 incr8d ; can run together or even before the previous instruction00007FF688D6A466 shrrax,1 ; this and the next can run at the same time00007FF688D6A469 andecx,1 ; both only depend on "mov rcx,rax"00007FF688D6A46C mov qword ptr [rbx+20h],rax ; we do writes, but we do not need to wait for them00007FF688D6A470 mov dword ptr [rbx+18h],r8d00007FF688D6A474 mov qword ptr [rsp+48h],rax ; not sure what is stored here, in other cases we have just 2 writes00007FF688D6A479 cmpr8d,40h00007FF688D6A47D jne GcInfoDecoder::EnumerateLiveSlots+0BEBh (07FF688D6A49Bh) 00007FF688D6A47F add qword ptr [rbx+10h],8 ; moving to the next word, relatively rare (1 in 64 reads)00007FF688D6A484 movrax,qword ptr [rbx+10h]00007FF688D6A488 xorr8d,r8d00007FF688D6A48B movrax,qword ptr [rax]00007FF688D6A48E mov qword ptr [rbx+20h],rax00007FF688D6A492 mov qword ptr [rsp+48h],rax00007FF688D6A497 mov dword ptr [rbx+18h],r8d00007FF688D6A49B testrcx,rcx00007FF688D6A49E je GcInfoDecoder::EnumerateLiveSlots+0BF3h (07FF688D6A4A3h) 00007FF688D6A4A0 incr13d00007FF688D6A4A3 subrdx,1

bool m_IsInterruptible;
bool m_IsVarArg;
bool m_GenericSecretParamIsMD;
bool m_GenericSecretParamIsMT;

@VSadovVSadovDec 19, 2023

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.

These are derived from masking the header flags. Masking is cheap and we may not even be asked for these, so we can do masking in the accessors.

// Use flag mask to bail out early if we already decoded all the pieces that caller requested
int remainingFlags = flags == DECODE_EVERYTHING ? ~0 : flags;

if (!slimHeader)

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.

Predecoding a fat header could be much more involved compared to slim headers, so let's deal with fat headers in a separate helper.

@VSadov

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@VSadov
VSadov marked this pull request as ready for review December 20, 2023 06:37
@VSadov

Copy link
Copy Markdown
MemberAuthor

I think this is ready for review.

@VSadov
VSadov requested a review from jkotasDecember 25, 2023 04:51
Comment threadsrc/coreclr/inc/gcinfodecoder.h

size_t result = (*m_pCurrent) & (((size_t)1) << m_RelPos);
size_t result = m_current & 1;
m_current >>= 1;

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.

Yes, but you pay for it by extra memory writes. Is it really a win at the end?

For example, here are timings for Intel 11th gen from
https://www.agner.org/optimize/instruction_tables.pdf (page 350):

| SHR SHL SAR r,i | 1 | 1 |
| SHR SHL SAR m,i | 4 | 4 |
| SHR SHL SAR r,cl | 2 | 2 |
| SHR SHL SAR m,cl | 5 | 6 |

Constant shift of memory is twice the cost of variable shift of register.

Comment threadsrc/coreclr/inc/gcinfodecoder.h
if(m_NumSafePoints == 0)
return false;

#if defined(TARGET_AMD64) || defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)

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.

Removal of this ifdef is changing behavior for Linux x86. I guess that it is a bug fix. I do not see a reason why safepoint handling should be different for Linux x86.

cc @gbalykov

@VSadovVSadovDec 28, 2023

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 assumed we can't get here with x86 since we use a different gcinfo encoder.
If that is not the case and we can on Linux, I will put this back.

Overall, I think this -1 hack is trying to solve a problem that does not exist, and it would be better if we could stop doing/undoing this artificial adjustment, but it is not relevant to this PR. I just thought that #if defined is always true here.

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.

@jkotas thanks for sharing this. Currently almost all clr tests fail on linux x86 on main branch, so there're other issues with it that need to be investigated first. We'll probably take a look at this change after that.

@jkotasjkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thank you!

@VSadov

VSadov commented Dec 28, 2023

Copy link
Copy Markdown
MemberAuthor

It looks like the linux-arm64 Release NativeAOT_Libraries is stuck in a loop repeating the same:

 Done building Helix work items. Work item count: 9
Starting Azure Pipelines Test Run net9.0-linux-Release-arm64-NativeAOT_Release-(Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8
Job e739cf8d-0586-4722-b4a3-61c7ae9aaf97 on (Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8 is completed with 10 finished work items.
Using _DebuggerHosts: chrome
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
BuildTargetFramework: net9.0
TestArchiveTestsRoot: /__w/1/s/artifacts/helix/tests/
TestArchiveRoot: /__w/1/s/artifacts/helix/
TestArchiveRuntimeRoot: /__w/1/s/artifacts/helix/runtime/
TestArchiveRuntimeFile: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Compressing runtime directory
Creating directory /__w/1/s/artifacts/helix/runtime/
Zipping directory "/__w/1/s/artifacts/bin/testhost/net9.0-linux-Release-arm64/" to "/__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip".
Using Queues: (Ubuntu.2204.Arm64.Open)Ubuntu.2004.ArmArch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-arm64v8+(Debian.11.Arm64.Open)Ubuntu.2004.Armarch.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-arm64v8
Build TargetFramework: net9.0
Building Helix work items
Using TestRunNamePrefix: net9.0-linux-Release-arm64-NativeAOT_Release-
Using HelixCorrelationPayload: /__w/1/s/artifacts/helix/runtime/test-runtime-net9.0-linux-Release-arm64.zip
Using HelixCommand: ./RunTests.sh --runtime-path "$HELIX_CORRELATION_PAYLOAD"
Using HelixType: test/functional/cli/innerloop/
Using WorkItemArchiveWildCard: /__w/1/s/artifacts/helix/tests/**/*.zip
Using Timeout: 00:45:00
Done building Helix work items. Work item count: 9

https://dev.azure.com/dnceng-public/public/_build/results?buildId=509866&view=logs&j=74bca3e6-08a5-5cef-0121-fe27fe79cc7b&t=317b4f3c-303e-557e-6c7e-66c36fe29909

It is not happening in my newer test run (#85694), so perhaps it is something that got fixed recently.

@VSadov
VSadov merged commit b4ba5da into dotnet:mainDec 28, 2023
@VSadov
VSadov deleted the dec branch December 28, 2023 21:44
@VSadov

Copy link
Copy Markdown
MemberAuthor

Thanks!!!

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 4, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrtenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@VSadov@jkotas@davidwrighton@gbalykov