libc: FILE* v2 — буферизация потоков (вариант B+)

- единый ленивый буфер BUFSIZ=512 на чтение и запись с
  автопереключением направления (_F_DIROUT, _file_sync: запись
  сбрасывается write()-ом, readahead откатывается lseek-ом)
- статическая таблица OPEN_MAX=8 слотов вместо malloc для FILE;
  _fclosall через atexit — exit() сбрасывает несброшенную запись
- fread/fwrite: мелкое через буфер (memcpy), блоки >= BUFSIZ — мимо
  буфера одним syscall; горячие пути fgetc/fputc и сканер строк
  fgets (LDI до '\n') — на asm, SDCC на эти цепочки генерит ~90
  инструкций с IX-фреймом
- новое: ungetc (1 байт через hold, работает и на stdin),
  fprintf/vfprintf (vsprintf+fwrite), fflush(NULL) = все потоки
- фиксы stdio-review: fwrite ставит _F_ERROR при короткой записи
  (issue 3), fgets(n=1) возвращает пустую строку (issue 4)
- замер (MAME, HDD, 100 КБ): небуферизованная оценка ~144 с →
  fgetc 5 с (×29), fgets ~1 с; дизайн и отвергнутые варианты —
  docs/file-buffering-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 16:09:13 +03:00
parent 4a081501d8
commit 48d552bf3a
27 changed files with 1093 additions and 302 deletions
+84
View File
@@ -0,0 +1,84 @@
/*
* fgets — прочитать строку (до n-1 символов, '\n' сохраняется) из
* потока в buf с завершающим NUL. Возвращает buf или NULL, если EOF
* наступил до первого сохранённого символа. n=1 по стандарту —
* валидный вызов: ничего не читает, возвращает пустую строку.
*
* Быстрый путь: пока строка лежит в readahead-буфере, она копируется
* блоком asm-сканером (LDI + сравнение с '\n', ~50 тактов/байт) —
* fgetc-на-символ был в ~10 раз медленнее (замер fbench). Медленные
* случаи (консоль, hold от ungetc, режим записи, дозаполнение буфера,
* EOF) уходят посимвольно через fgetc.
*/
#include "_file.h"
/* Копировать из src в dst до max байт, остановившись ПОСЛЕ '\n'.
* Возврат: число скопированных байт | 0x8000, если '\n' найден.
* max: 1..512 (бит 15 счётчика свободен). */
static uint16_t fgets_scan(uint8_t *dst, const uint8_t *src,
uint16_t max) __naked
{
(void)dst; (void)src; (void)max;
__asm
;; HL = dst, DE = src, max на стеке (callee чистит).
pop iy ; адрес возврата
pop bc ; BC = max (> 0)
ex de, hl ; HL = src, DE = dst
push bc ; max понадобится для подсчёта
_fgs_loop:
ld a, (hl)
cp a, #0x0A ; сравнить ДО ldi: LDI не трогает Z,
ldi ; но выставляет P/V по BC ((DE)<-(HL))
jr Z, _fgs_found ; Z от cp
jp PE, _fgs_loop ; P/V от LDI: BC != 0
;; max исчерпан, '\n' нет: count = max
pop de
jp (iy)
_fgs_found:
;; count = max - BC, бит 15 = флаг '\n' (count <= 512 безопасно)
pop hl ; HL = max
ld a, l
sub a, c
ld e, a
ld a, h
sbc a, b
or a, #0x80
ld d, a
jp (iy)
__endasm;
}
char *fgets(char *buf, int n, FILE *fp)
{
if (!buf || n < 1 || !fp) return NULL;
uint8_t *dst = (uint8_t *)buf;
int space = n - 1;
while (space > 0) {
if (fp->level && !(fp->flags & _F_DIROUT) && fp->hold == EOF) {
/* Быстрый блок: сканировать readahead до '\n'. */
uint16_t take = fp->level;
if (take > (uint16_t)space) take = (uint16_t)space;
uint16_t got = fgets_scan(dst, fp->curp, take);
uint16_t cnt = got & 0x7FFF;
fp->curp += cnt;
fp->level -= cnt;
dst += cnt;
space -= (int)cnt;
if (got & 0x8000) break; /* строка закончена */
continue; /* буфер иссяк — дозаполнить */
}
/* Медленные случаи + refill/EOF — посимвольно. */
int c = fgetc(fp);
if (c == EOF) {
if (dst == (uint8_t *)buf) return NULL;
break;
}
*dst++ = (uint8_t)c;
space--;
if (c == '\n') break;
}
*dst = '\0';
return buf;
}