Performance Mystery: Modern CPU's under-perform?...
From
BGB@cr88192@gmail.com to
comp.arch on Tue Jul 28 23:19:38 2026
From Newsgroup: comp.arch
Otherwise, I am faced with a little bit of a recent mystery:
I had recently decided to port the TestKern kernel from XG1 to XG3;
Initially, TKRA-GL was kinda broken;
I have now gotten TKRA-GL working again, but in the process noted a mystery.
If using software rasterization in both XG3 and Win64, using the same
general C code, the relative performance difference was much smaller
than expected...
Like, TKRA-GL with a plain C rasterizer on an emulated 50 MHz isn't
exactly fast...
But, seemingly, only 8x slower than a native Win64 build, despite the
roughly 74x CPU speed difference.
When it was XG1 with all the ASM and special helpers, was like "Yeah,
these helpers sure are amazing". When in a plain C path though, the
helpers are less significant.
Then I looked into it, the mystery shifted, no longer:
"Why is my custom CPU core seemingly so faster";
And instead:
"Why is my desktop PC's CPU seemingly so slow at this?..."
The biggest area of difference is seemingly in the span drawing functions:
XG3: They are significant, but not what eats all the CPU time;
X64: The span drawing functions eat a whole lot of CPU.
Like, one would think:
Either native Win64 would be blazing fast;
Or, the 50MHz XG3 version should be glacial.
Like, when it doesn't have the "assist" of being all ASM with a whole
lot of special helpers, etc, as in the XG1 build.
( Though, yeah, I had since implemented a few ASM paths for XG3 as well,
but still mostly using the C versions of the edge-walk and span drawing
logic for now... ).
There was a difference in 320x200 vs 640x480, but by most measures this contributes a roughly 5 factor (at 320x200, it would be holding 60 fps
on the Win64 build, but this makes measurement harder).
Does seem like there a few hotspots:
Z-test and pixel store;
Checking for cache-hit in the texel fetch;
And returning from texel fetch;
...
Then again, this code takes a pretty big hit on RV64G, but this more has
to do with a while lot more spill-and-fill.
Lots of spill-and-fill on x64, but doesn't seem like spill and fill can account for this. I don't really know the code MSVC generates though, as Visual Studio is a little bit of a black-box in this way.
Curiously also, the relative added overhead of running XG3 in an
emulator is relatively modest in this case (though, for running TKRA-GL,
the emulator is hard-pushed to emulate much over 80MHz or so; seemingly
around 4x slower than native in this case).
All this does seem kinda weird, like if the rasterizer is hard, why then
is the emulator itself not slower?... (Note: No JIT in this case).
It is like, there is some mysterious bottleneck that is resisting an
easy explanation. It is just like it is being comparably slow for no
obvious reason (on a Zen+ based CPU).
Well, Grok claimed that the scenario was due to all of this creating a
big sequential-dependency chain which the OoO CPUs can't really work
around. I am not sure how much weight I would put on this (would imply
that the OoO CPUs effectively have an absurdly high internal latency
relative to in-order CPUs; but if so, I would expect it would be visible
in many other contexts as well besides just "why is my software renderer
kinda slow?...").
Another possibility could be if the emulator was running faster than
expected, but it counts cycles and tries to be accurate here, and in the
past tests hadn't tended to disagree significantly from when running it
on an FPGA or in a Verilator simulation, ...
So, ...
Or, a more specific example, one of the significant offending code paths (there are others, but this is one of the more highly used ones).
There are also Alpha-Blended and Alpha-Test versions, etc, but they are
less used (note that generic GL_BLEND falls back to a different path as
well, this function being for opaque textured geometry with color
modulation). Note that the step values would have been calculated in the edge-walker function (not listed here).
<===
void TKRA_DrawSpan_ModBlUtx2MortZb(u64 *parm,
tkra_rastpixel *dstc, tkra_zbufpixel *dstz, int cnt)
{
tkra_rastpixel *ct, *cte, *src;
tkra_zbufpixel *ctz;
u64 tpos, tstep, tpos1, tstep1;
u64 cpos, cstep;
u64 zpos, zstep;
u64 cval;
u32 xmask, ymask;
s32 z;
int ix0, ix1, ix2, ix3;
u64 pix0, pix1, pix2, pix3;
int pix, clr, idx;
tpos=parm[TKRA_DS_TPOS];
tstep=parm[TKRA_DS_TSTEP];
cpos=parm[TKRA_DS_CPOS];
cstep=parm[TKRA_DS_CSTEP];
zpos=parm[TKRA_DS_ZPOS];
zstep=parm[TKRA_DS_ZSTEP];
src=(tkra_rastpixel *)(parm[TKRA_DS_TEXBCN]);
xmask=parm[TKRA_DS_XMASK];
ymask=parm[TKRA_DS_YMASK];
tstep1=0x0001000000010000ULL;
ct=dstc; cte=ct+cnt;
ctz=dstz;
while(ct<cte)
{
z=zpos>>16;
if(z>(*ctz)) //hot-ish
{
ctz++;
ct++;
tpos+=tstep;
cpos+=cstep;
zpos+=zstep;
continue;
}
ix2=(tpos>>16); ix3=(tpos>>48);
ix0=tkra_morton16(ix2+0, ix3+0)&ymask;
ix1=tkra_morton16(ix2+1, ix3+0)&ymask;
ix2=tkra_morton16(ix2+0, ix3+1)&ymask;
pix0=TKRA_CachedBlkUtx2(src, ix0); //hot
pix1=TKRA_CachedBlkUtx2(src, ix1); //hot
pix2=TKRA_CachedBlkUtx2(src, ix2); //hot
cval=TKRA_InterpBilinear3Pt_64(pix0, pix1, pix2,
(u16)tpos, (u16)(tpos>>32));
cval=tkra_pmuluhw(cval, cpos);
pix=tkra_rgbpck64(cval);
z=zpos>>16;
if(z<=(*ctz))
{
*ct=pix; //hot
*ctz=z; //hot
}
ctz++;
ct++;
tpos+=tstep;
cpos+=cstep;
zpos+=zstep;
}
}
int tkra_morton16(int x, int y)
{
return(
(pmorttab[(x )&255] )|(pmorttab[(y )&255]<< 1)|
(pmorttab[(x>>8)&255]<<16)|(pmorttab[(y>>8)&255]<<17));
}
int tkra_utx2_cachedindx[64];
void *tkra_utx2_cachedblka[64];
u64 tkra_utx2_cachedpels[64][16];
u64 TKRA_CachedBlkUtx2(void *src, int ix)
{
u64 *blka;
u64 tca[4];
int pxa, pxb, pxv;
int axa, axb, axc;
u64 blk;
u64 clra, clrb, clrc, clrd, clrp, clrq;
int ix0, ix1, hxi;
int i;
if(!src)
{
return(0);
}
ix0=ix&15;
ix1=ix>>4;
hxi=(ix1^(ix1>>6))&63;
if( (tkra_utx2_cachedindx[hxi]==ix1) &&
(tkra_utx2_cachedblka[hxi]==src)) //hot
{
return(tkra_utx2_cachedpels[hxi][ix0]); //hot
}
/* rest is a cooler path, only hit if texture block misses */
blka=src;
blk=blka[ix1];
pxa=(u16)(blk>> 0);
pxb=(u16)(blk>>16);
pxv=blk>>32;
clra=tkra_rgbupck64(pxa);
clrb=tkra_rgbupck64(pxb);
clrc=tkra_pmuluhw(clra, 0xAAAAAAAAAAAAAAAAULL);
clrd=tkra_pmuluhw(clrb, 0xAAAAAAAAAAAAAAAAULL);
clrp=tkra_pmuluhw(clra, 0x5555555555555555ULL);
clrq=tkra_pmuluhw(clrb, 0x5555555555555555ULL);
clrc+=clrq;
clrd+=clrp;
tca[0]=clrb;
tca[1]=clrd;
tca[2]=clrc;
tca[3]=clra;
if((pxa&0x8000) && (pxb&0x8000))
{
for(i=0; i<16; i++)
{
clrp=tca[(pxv>>(i*2))&3];
tkra_utx2_cachedpels[hxi][i]=clrp;
}
}else
if(pxb&0x8000)
{
axa=TKRA_GetPixel444A3_Alpha(pxa);
axb=TKRA_GetPixel444A3_Alpha(pxb);
axa=axa<<8;
axb=axb<<8;
for(i=0; i<16; i++)
{
clrp= ((pxv>>(i*2+1))&1)?clra:clrb;
axc = ((pxv>>(i*2+0))&1)?axa:axb;
clrp&=0x0000FFFFFFFFFFFFULL;
clrp|=((u64)axc)<<48;
tkra_utx2_cachedpels[hxi][i]=clrp;
}
}else
{
for(i=0; i<16; i++)
{
clrp=tca[(pxv>>(i*2))&3];
tkra_utx2_cachedpels[hxi][i]=clrp;
}
}
tkra_utx2_cachedindx[hxi]=ix1;
tkra_utx2_cachedblka[hxi]=src;
return(tkra_utx2_cachedpels[hxi][ix0]);
}
u64 TKRA_InterpBilinear3Pt_64(
u64 px0, u64 px1, u64 px2,
u16 xfrac, u16 yfrac)
{
u64 cv4, cv5, cv6;
u64 cxf, cyf, cxnf, cynf, cxynf, cxf2, cyf2;
cxf=xfrac; cxf|=cxf<<16; cxf|=cxf<<32;
cyf=yfrac; cyf|=cyf<<16; cyf|=cyf<<32;
cxnf=~cxf;
cynf=~cyf;
cxf2=((cxf>>1)&0x7FFE7FFE7FFE7FFEULL);
cyf2=((cyf>>1)&0x7FFE7FFE7FFE7FFEULL);
cxynf=~(cxf2+cyf2);
cv5=tkra_pmuluhw(px0, cxynf)+
tkra_pmuluhw(px1, cxf2)+
tkra_pmuluhw(px2, cyf2);
return(cv5);
}
... Rest is mostly stuff like SIMD wrappers and similar ...
--- Synchronet 3.22a-Linux NewsLink 1.2
From
BGB@cr88192@gmail.com to
comp.arch on Thu Jul 30 04:14:12 2026
From Newsgroup: comp.arch
On 7/30/2026 2:00 AM, Lawrence DrCOOliveiro wrote:
On Tue, 28 Jul 2026 23:19:38 -0500, BGB wrote:
Like, one would think:
Either native Win64 would be blazing fast;
Or, the 50MHz XG3 version should be glacial.
DonrCOt trust performance measurements obtained under Microsoft Windows.
You really have no idea what the variables are in the OS overhead,
given that the internals of Windows are completely opaque.
This would be more true if the majority of the time were going into
something under the control of the OS itself...
When it is more straight up a case of "I am profiling ye olde C via the
Visual Studio profiler", it is mostly reliable.
Rendering inside of a custom rasterizer implementing an OpenGL
interface, but backed up with a software rasterizer, is still mostly
within "plain old C" domain.
I did just go and poke around some more, and realized the explanation
was more mundane, and a lot more dumb:
I was building the Win64 GLQuake as "/Os"...
Turns out "/Os" in this case was around 5x slower than "/O1".
So, switching to "/O1", it is within a factor-of-2 of the expected performance.
Comparing "/O1" vs "/O2", very little additional difference; both
pulling off around 50-70 fps in software rasterized GLQuake at 640x480 ...
Looking more, it seems it was a case of me misunderstanding what "/Os"
did, I was mistakenly just sorta thinking it was like "-Os" in GCC (it
is not...). Which would sorta explain why it also seems to fail to
effectively produce small binaries. By itself it is basically just doing
the same thing as a debug build.
So yeah, seems it was less "great mystery" and more "dumb compiler
flags...".
Though usually debug-vs-optimized build speeds aren't quite so drastic...
--- Synchronet 3.22a-Linux NewsLink 1.2