• 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 Lawrence =?iso-8859-13?q?D=FFOliveiro?=@ldo@nz.invalid to comp.arch on Thu Jul 30 07:00:54 2026
    From Newsgroup: comp.arch

    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.
    --- 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