Benchmark the boundary and close the cold read's tutorial gaps
The interop test now ends with a measured comparison: 24,000 formation updates through the script boundary against a line-for-line C translation of the same state machine. 881 us against 0.01 us per call on this machine, quoted verbatim in the new chapter 21 Step 11 with the architectural decisions it prices. A Haiku-class cold read of the chapters produced a build whose failures were all mechanical -- invented include paths, never-shown sink statics, guessed status codes and character names. The chapters now carry the include lists, the script.c statics, the status-code roster, the sprite/character table, the full CMake recipe and the explosion spawn's HANDLE example, so none of those have to be guessed again. Co-authored-by: andrew <andrew@aklabs.net> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiGgpHuXUm2mR4Wzndw3dc
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
@@ -115,12 +116,144 @@ static akerr_ErrorContext *run_claims(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ benchmark --- */
|
||||
|
||||
/**
|
||||
* @brief UPDATEBEE's state machine, translated line for line into C.
|
||||
*
|
||||
* The native comparator for the benchmark below: the same guard, the same
|
||||
* three branches, the same arithmetic as galaga.bas's UPDATEBEE with its
|
||||
* helpers inlined. Nothing is simplified, so the timing difference is the
|
||||
* interpreter's, not the algorithm's.
|
||||
*/
|
||||
static void native_updatebee(galaga_Enemy *enemy, akgl_Actor *actor, float dt)
|
||||
{
|
||||
float dx = 0.0f;
|
||||
float dy = 0.0f;
|
||||
float k = 0.0f;
|
||||
int32_t s = 0;
|
||||
|
||||
enemy->t += dt;
|
||||
if ( enemy->t < 0.0f ) {
|
||||
return;
|
||||
}
|
||||
s = enemy->state;
|
||||
if ( (s & GALAGA_ES_ENTERING) != 0 ) {
|
||||
dx = enemy->homex - actor->x;
|
||||
dy = enemy->homey - actor->y;
|
||||
k = dt * 4.5f;
|
||||
if ( k > 1.0f ) {
|
||||
k = 1.0f;
|
||||
}
|
||||
actor->x += dx * k + sinf(enemy->t * 6.0f) * 90.0f * dt;
|
||||
actor->y += dy * k;
|
||||
if ( fabsf(dx) < 3.0f && fabsf(dy) < 3.0f ) {
|
||||
enemy->state = GALAGA_ES_FORMATION;
|
||||
enemy->t = 0.0f;
|
||||
}
|
||||
}
|
||||
if ( (s & GALAGA_ES_FORMATION) != 0 ) {
|
||||
actor->x = enemy->homex + sinf(enemy->t * 1.7f) * 16.0f;
|
||||
actor->y = enemy->homey;
|
||||
if ( enemy->rnd < dt * 0.04f ) {
|
||||
enemy->state = GALAGA_ES_DIVING;
|
||||
enemy->t = 0.0f;
|
||||
}
|
||||
}
|
||||
if ( (s & GALAGA_ES_DIVING) != 0 ) {
|
||||
actor->y += (enemy->t * 150.0f + 260.0f) * dt;
|
||||
actor->x += sinf(enemy->t * 4.0f) * 130.0f * dt;
|
||||
dx = galaga_shared.playerx - actor->x;
|
||||
if ( dx > 220.0f ) {
|
||||
dx = 220.0f;
|
||||
}
|
||||
if ( dx < -220.0f ) {
|
||||
dx = -220.0f;
|
||||
}
|
||||
actor->x += dx * 0.2f * dt;
|
||||
if ( actor->y > 1040.0f ) {
|
||||
actor->y = -90.0f;
|
||||
enemy->state = GALAGA_ES_ENTERING;
|
||||
enemy->t = 0.0f;
|
||||
}
|
||||
dx = galaga_shared.playerx - actor->x;
|
||||
if ( fabsf(dx) <= 140.0f && actor->y <= galaga_shared.playery
|
||||
&& enemy->rnd < dt * 1.5f ) {
|
||||
enemy->fire = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static double seconds_since(const struct timespec *t0)
|
||||
{
|
||||
struct timespec t1;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
return (double)(t1.tv_sec - t0->tv_sec) + (double)(t1.tv_nsec - t0->tv_nsec) / 1e9;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The cost of thinking in BASIC, measured against the same logic in C.
|
||||
*
|
||||
* Both loops run the identical formation-hold workload, 24,000 calls -- forty
|
||||
* enemies at sixty frames a second for ten seconds. Informational: nothing
|
||||
* asserts on the timing, because CI machines vary; the numbers print so the
|
||||
* tutorial can quote a real measurement.
|
||||
*/
|
||||
static akerr_ErrorContext *run_benchmark(void)
|
||||
{
|
||||
galaga_Enemy enemy;
|
||||
akgl_Actor actor;
|
||||
struct timespec t0;
|
||||
double basic_s = 0.0;
|
||||
double native_s = 0.0;
|
||||
int i = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
memset(&enemy, 0, sizeof(enemy));
|
||||
memset(&actor, 0, sizeof(actor));
|
||||
enemy.kind = GALAGA_ENEMY_BEE;
|
||||
enemy.state = GALAGA_ES_FORMATION;
|
||||
enemy.homex = 400.0f;
|
||||
enemy.homey = 300.0f;
|
||||
enemy.rnd = 0.9f;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
for ( i = 0; i < 24000; i++ ) {
|
||||
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||
}
|
||||
basic_s = seconds_since(&t0);
|
||||
|
||||
memset(&enemy, 0, sizeof(enemy));
|
||||
memset(&actor, 0, sizeof(actor));
|
||||
enemy.kind = GALAGA_ENEMY_BEE;
|
||||
enemy.state = GALAGA_ES_FORMATION;
|
||||
enemy.homex = 400.0f;
|
||||
enemy.homey = 300.0f;
|
||||
enemy.rnd = 0.9f;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
for ( i = 0; i < 24000; i++ ) {
|
||||
native_updatebee(&enemy, &actor, 0.016f);
|
||||
}
|
||||
native_s = seconds_since(&t0);
|
||||
|
||||
printf("benchmark: 24000 formation-hold updates, dt 0.016\n");
|
||||
printf(" BASIC through the boundary: %8.3f s %7.2f us/call %6.3f ms per 40-enemy frame\n",
|
||||
basic_s, basic_s / 24000.0 * 1e6, basic_s / 24000.0 * 40.0 * 1e3);
|
||||
printf(" the same logic in C: %8.3f s %7.2f us/call %6.3f ms per 40-enemy frame\n",
|
||||
native_s, native_s / 24000.0 * 1e6, native_s / 24000.0 * 40.0 * 1e3);
|
||||
printf(" ratio: %.0fx\n", basic_s / native_s);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, run_claims());
|
||||
CATCH(errctx, run_benchmark());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
|
||||
Reference in New Issue
Block a user