feat(rtl): migrate blaise_utf8 .s to inline asm — RTL now pure Pascal
This commit is contained in:
parent
52fd1d5b3b
commit
0c4a22f5e2
|
|
@ -5,19 +5,14 @@
|
|||
# Licensed under the Apache License v2.0 with Runtime Library Exception.
|
||||
# See LICENSE file in the project root for full license terms.
|
||||
#
|
||||
# Each Pascal unit is compiled directly via Blaise's unit-as-top-level
|
||||
# mode (project_unit_as_toplevel, 2026-05-24): a single
|
||||
# `blaise --source X.pas --output X.o` invocation runs qbe + cc -c
|
||||
# + objcopy-embed in one step. No build-driver shims, no IR-strip
|
||||
# sed pipelines, no intermediate .ssa/.s files needed.
|
||||
# Each RTL unit is compiled directly via Blaise's unit-as-top-level mode: a
|
||||
# single `blaise --source X.pas --output X.o` invocation produces the object.
|
||||
# The RTL is now entirely pure Pascal (the four hand-written *_x86_64.s files
|
||||
# were migrated to inline-asm `.pas` units), so the whole archive builds through
|
||||
# Blaise's own internal assembler — no external assembler, no gcc, no .s files.
|
||||
SHELL = /bin/bash
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -O2 -Wall -Wextra -std=c11
|
||||
|
||||
SRC_DIR = src/main/c
|
||||
PAS_SRC_DIR = src/main/pascal
|
||||
ASM_DIR = src/main/asm
|
||||
OBJ_DIR = target
|
||||
LIB = $(OBJ_DIR)/blaise_rtl.a
|
||||
|
||||
|
|
@ -36,18 +31,16 @@ BLAISE = $(COMPILER_BIN)/blaise
|
|||
#
|
||||
# --assembler internal: assemble each unit to its .o with Blaise's own in-process
|
||||
# assembler instead of `cc -c`, so building the RTL needs no external assembler.
|
||||
# (The hand-written .s files in $(ASM_DIR) still go through $(CC) until Blaise can
|
||||
# assemble a standalone .s; the Pascal units no longer touch gcc.)
|
||||
BLAISE_FLAGS = --no-incremental --assembler internal
|
||||
|
||||
# Assembly sources — platform-specific, no C compiler needed.
|
||||
# (blaise_atomic, blaise_setjmp, blaise_start migrated to inline-asm units.)
|
||||
ASM_OBJS = $(OBJ_DIR)/blaise_utf8_x86_64.o
|
||||
|
||||
# Pascal RTL units compiled via the Blaise compiler.
|
||||
# Every RTL unit is now pure Pascal (the four hand-written *_x86_64.s files —
|
||||
# start, setjmp, atomic, utf8 — were migrated to inline-asm `.pas` units). The
|
||||
# RTL therefore builds entirely through Blaise's own pipeline: no $(CC), no
|
||||
# external assembler, no .s files.
|
||||
PAS_OBJS = $(OBJ_DIR)/blaise_start.o \
|
||||
$(OBJ_DIR)/blaise_atomic.o \
|
||||
$(OBJ_DIR)/blaise_setjmp.o \
|
||||
$(OBJ_DIR)/blaise_utf8.o \
|
||||
$(OBJ_DIR)/blaise_mem.o \
|
||||
$(OBJ_DIR)/blaise_str.o \
|
||||
$(OBJ_DIR)/blaise_set.o \
|
||||
|
|
@ -59,7 +52,7 @@ PAS_OBJS = $(OBJ_DIR)/blaise_start.o \
|
|||
$(OBJ_DIR)/rtl_platform_layout_linux.o \
|
||||
$(OBJ_DIR)/rtl_platform_posix.o
|
||||
|
||||
OBJS = $(ASM_OBJS) $(PAS_OBJS)
|
||||
OBJS = $(PAS_OBJS)
|
||||
|
||||
.PHONY: all install clean
|
||||
|
||||
|
|
@ -68,12 +61,11 @@ all: $(LIB)
|
|||
$(LIB): $(OBJS)
|
||||
ar rcs $@ $^
|
||||
|
||||
# --- Assembly rules ---
|
||||
$(OBJ_DIR)/%.o: $(ASM_DIR)/%.s
|
||||
@mkdir -p $(OBJ_DIR)
|
||||
$(CC) -c -o $@ $<
|
||||
|
||||
# --- Pascal unit rules (unit-as-top-level) ---
|
||||
$(OBJ_DIR)/blaise_utf8.o: $(PAS_SRC_DIR)/blaise_utf8.pas
|
||||
@mkdir -p $(OBJ_DIR)
|
||||
$(BLAISE) $(BLAISE_FLAGS) --source $< --unit-path $(PAS_SRC_DIR) --output $@
|
||||
|
||||
$(OBJ_DIR)/blaise_start.o: $(PAS_SRC_DIR)/blaise_start.pas
|
||||
@mkdir -p $(OBJ_DIR)
|
||||
$(BLAISE) $(BLAISE_FLAGS) --source $< --unit-path $(PAS_SRC_DIR) --output $@
|
||||
|
|
|
|||
|
|
@ -1,226 +0,0 @@
|
|||
#
|
||||
# Blaise — An Object Pascal Compiler
|
||||
# Copyright (c) 2026 Graeme Geldenhuys
|
||||
# SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
|
||||
# Licensed under the Apache License v2.0 with Runtime Library Exception.
|
||||
# See LICENSE file in the project root for full license terms.
|
||||
#
|
||||
# SIMD-accelerated UTF-8 codepoint counting (x86_64).
|
||||
#
|
||||
# _Utf8CountCodePoints counts the number of UTF-8 codepoints in a byte buffer
|
||||
# by counting non-continuation bytes (bytes where (b & 0xC0) != 0x80).
|
||||
#
|
||||
# Strategy: use AVX2 (32 bytes/iteration) when available, fall back to SSE2
|
||||
# (16 bytes/iteration), then scalar for tail bytes.
|
||||
#
|
||||
# Calling convention (System V AMD64):
|
||||
# %rdi = pointer to first byte of string data
|
||||
# %esi = byte length of the string
|
||||
# Returns codepoint count in %eax.
|
||||
#
|
||||
|
||||
.text
|
||||
|
||||
.globl _Utf8CountCodePoints
|
||||
.type _Utf8CountCodePoints, @function
|
||||
_Utf8CountCodePoints:
|
||||
# Fast path: empty or very short strings
|
||||
testl %esi, %esi
|
||||
jle .Lreturn_zero
|
||||
|
||||
movl %esi, %ecx # ecx = remaining bytes
|
||||
xorl %eax, %eax # eax = codepoint count accumulator
|
||||
xorl %edx, %edx # edx = current offset
|
||||
|
||||
# Check for AVX2 support via CPUID (cached in .Lavx2_flag)
|
||||
cmpl $0, .Lavx2_checked(%rip)
|
||||
jne .Lavx2_known
|
||||
call .Lcheck_avx2
|
||||
xorl %eax, %eax # restore accumulator (cpuid clobbers eax)
|
||||
.Lavx2_known:
|
||||
cmpl $0, .Lavx2_flag(%rip)
|
||||
je .Lsse2_entry
|
||||
|
||||
# --- AVX2 path: 32 bytes per iteration ---
|
||||
cmpl $32, %ecx
|
||||
jb .Lsse2_entry
|
||||
|
||||
# 0x80 broadcast into ymm1 (continuation byte marker)
|
||||
vpcmpeqb %ymm2, %ymm2, %ymm2 # ymm2 = all 0xFF
|
||||
vpsrlw $1, %ymm2, %ymm2 # ymm2 = 0x7F7F...
|
||||
vpaddw %ymm2, %ymm2, %ymm1 # This doesn't give 0x80 correctly
|
||||
# Correct approach: use vpbroadcastb-equivalent via stack
|
||||
# Actually: 0x80 = set top bit of each byte
|
||||
vpcmpeqb %ymm2, %ymm2, %ymm2 # ymm2 = all 0xFF
|
||||
vpsllw $7, %ymm2, %ymm1 # ymm1 = 0x80 in each byte position
|
||||
# That gives 0x80,0x00 pattern in words. Need per-byte.
|
||||
# Use vpabsb on all-ones to get 0x01, then shift.
|
||||
# Simplest: build 0xC0 and 0x80 masks on stack.
|
||||
|
||||
# Better: count bytes where (byte & 0xC0) != 0x80
|
||||
# Equivalent: count bytes where byte < 0x80 OR byte >= 0xC0
|
||||
# Which is: NOT a continuation byte (10xxxxxx pattern)
|
||||
# A continuation byte has bits 7:6 == 10.
|
||||
# (byte + 0x40) sets bit 7 for 0x80..0xBF range → overflow into 0xC0..0xFF
|
||||
# Then test bit 7: if set = NOT continuation.
|
||||
# Actually simplest SIMD approach: byte >= 0xC0 OR byte < 0x80
|
||||
# = saturating subtract 0x80, then compare > 0x3F (catches >= 0xC0)
|
||||
# OR original byte < 0x80
|
||||
# Even simpler: (byte & 0xC0) != 0x80
|
||||
# → XOR with 0x80 → (byte ^ 0x80) & 0xC0 != 0 iff not continuation
|
||||
# → shift right by 6 and sum? No.
|
||||
#
|
||||
# Standard trick: count bytes that are NOT 10xxxxxx.
|
||||
# A byte is a lead/ASCII byte iff its top two bits are NOT 10.
|
||||
# pcmpgtb is signed comparison, so:
|
||||
# (signed) byte > -65 (0xBF as signed = -65) means byte >= 0xC0
|
||||
# (signed) byte > -1 means byte >= 0 means byte < 0x80 (ASCII)... no.
|
||||
#
|
||||
# Classic UTF-8 SIMD count:
|
||||
# For each byte B: is_not_continuation = ((B & 0xC0) != 0x80)
|
||||
# Using signed arithmetic:
|
||||
# Treat bytes as signed: continuation bytes 0x80..0xBF = -128..-65
|
||||
# if (signed_byte > -65) → NOT continuation (includes 0xC0..0xFF and 0x00..0x7F)
|
||||
# pcmpgtb ymm_data, ymm_threshold(-65) → 0xFF for non-continuation bytes
|
||||
#
|
||||
# Wait — pcmpgtb(a, b) = a > b? No: pcmpgtb dest, src means dest[i] = (dest[i] > src[i]) ? 0xFF : 0x00
|
||||
# We want: data[i] > -65 → is_not_continuation
|
||||
# So we need pcmpgtb with data in dest and -65 broadcast in src.
|
||||
# But pcmpgtb is destructive. So:
|
||||
# 1. Load data into ymm0
|
||||
# 2. Set ymm1 = broadcast(-65) = 0xBF bytes
|
||||
# 3. vpcmpgtb ymm2, ymm0, ymm1 → ymm2[i] = 0xFF if data[i] > -65 (signed)
|
||||
# 4. vpsrlw $7 on result... no, we need to count the 0xFF bytes.
|
||||
# 5. Sum: vpsubb zeros, ymm2 (subtracting 0xFF = adding 1 per set byte)
|
||||
# Actually -(-1) = 1, so vpsubb(0, mask) counts ones.
|
||||
# But vpsubb has no immediate form; use pabsb on the result (abs(-1)=1, abs(0)=0)
|
||||
# vpsabsb ymm2, ymm2 → each byte is 0 or 1
|
||||
# 6. vpsadbw to horizontally sum bytes → 4 x 16-bit sums in qword lanes
|
||||
|
||||
# Build threshold: broadcast -65 (0xBF as signed) into ymm1
|
||||
# -65 = 0xBF. All bytes 0xBF.
|
||||
vpcmpeqb %ymm1, %ymm1, %ymm1 # ymm1 = all 0xFF
|
||||
# We need 0xBF = ~0x40. Start from 0xFF, add 0xC0? No.
|
||||
# 0xBF = 0xFF - 0x40. Use vpaddb with -0x40? We don't have that.
|
||||
# Simplest: load from memory constant.
|
||||
vmovdqa .Lthreshold_avx(%rip), %ymm1
|
||||
|
||||
vpxor %ymm3, %ymm3, %ymm3 # ymm3 = zero (accumulator)
|
||||
|
||||
.Lavx2_loop:
|
||||
vmovdqu (%rdi, %rdx), %ymm0 # load 32 bytes
|
||||
vpcmpgtb %ymm1, %ymm0, %ymm2 # ymm2[i] = 0xFF if data[i] > -65 (signed)
|
||||
vpabsb %ymm2, %ymm2 # ymm2[i] = 0 or 1
|
||||
vpaddb %ymm2, %ymm3, %ymm3 # accumulate byte counts (safe for 255 iters)
|
||||
|
||||
addl $32, %edx
|
||||
subl $32, %ecx
|
||||
cmpl $32, %ecx
|
||||
jge .Lavx2_loop
|
||||
|
||||
# Horizontal sum of ymm3: vpsadbw → sum each 8-byte lane into 64-bit
|
||||
vpxor %ymm4, %ymm4, %ymm4
|
||||
vpsadbw %ymm4, %ymm3, %ymm3 # ymm3 = 4 x 64-bit partial sums
|
||||
# Extract and sum the 4 quadwords
|
||||
vextracti128 $1, %ymm3, %xmm4
|
||||
vpaddq %xmm4, %xmm3, %xmm3 # xmm3 = 2 x 64-bit sums
|
||||
vpshufd $0x0E, %xmm3, %xmm4 # move high qword to low
|
||||
vpaddq %xmm4, %xmm3, %xmm3
|
||||
vmovd %xmm3, %r8d
|
||||
addl %r8d, %eax
|
||||
|
||||
vzeroupper
|
||||
jmp .Lsse2_entry
|
||||
|
||||
# --- SSE2 path: 16 bytes per iteration ---
|
||||
.Lsse2_entry:
|
||||
cmpl $16, %ecx
|
||||
jb .Lscalar_entry
|
||||
|
||||
# Build threshold -65 (0xBF) in xmm1
|
||||
movdqa .Lthreshold_sse(%rip), %xmm1
|
||||
pxor %xmm3, %xmm3 # xmm3 = zero accumulator
|
||||
|
||||
.Lsse2_loop:
|
||||
movdqu (%rdi, %rdx), %xmm0 # load 16 bytes
|
||||
movdqa %xmm0, %xmm2 # copy (pcmpgtb is destructive)
|
||||
pcmpgtb %xmm1, %xmm2 # xmm2[i] = 0xFF if data[i] > -65
|
||||
pabsb %xmm2, %xmm2 # xmm2[i] = 0 or 1
|
||||
paddb %xmm2, %xmm3 # accumulate (safe for 255 iters)
|
||||
|
||||
addl $16, %edx
|
||||
subl $16, %ecx
|
||||
cmpl $16, %ecx
|
||||
jge .Lsse2_loop
|
||||
|
||||
# Horizontal sum of xmm3
|
||||
pxor %xmm4, %xmm4
|
||||
psadbw %xmm4, %xmm3 # xmm3 = 2 x 64-bit sums
|
||||
movd %xmm3, %r8d
|
||||
pshufd $0x0E, %xmm3, %xmm3
|
||||
movd %xmm3, %r9d
|
||||
addl %r8d, %eax
|
||||
addl %r9d, %eax
|
||||
|
||||
# --- Scalar tail: remaining < 16 bytes ---
|
||||
.Lscalar_entry:
|
||||
testl %ecx, %ecx
|
||||
jle .Lreturn
|
||||
|
||||
.Lscalar_loop:
|
||||
movzbl (%rdi, %rdx), %r8d # load one byte
|
||||
# Non-continuation if (byte & 0xC0) != 0x80
|
||||
movl %r8d, %r9d
|
||||
andl $0xC0, %r9d
|
||||
cmpl $0x80, %r9d
|
||||
setne %r9b
|
||||
movzbl %r9b, %r9d
|
||||
addl %r9d, %eax
|
||||
incl %edx
|
||||
decl %ecx
|
||||
jnz .Lscalar_loop
|
||||
|
||||
.Lreturn:
|
||||
ret
|
||||
|
||||
.Lreturn_zero:
|
||||
xorl %eax, %eax
|
||||
ret
|
||||
|
||||
# --- AVX2 detection subroutine ---
|
||||
.Lcheck_avx2:
|
||||
pushq %rbx
|
||||
pushq %rcx
|
||||
pushq %rdx
|
||||
# CPUID leaf 7, subleaf 0 — bit 5 of EBX = AVX2
|
||||
movl $7, %eax
|
||||
xorl %ecx, %ecx
|
||||
cpuid
|
||||
testl $(1 << 5), %ebx
|
||||
setne %al
|
||||
movzbl %al, %eax
|
||||
movl %eax, .Lavx2_flag(%rip)
|
||||
movl $1, .Lavx2_checked(%rip)
|
||||
popq %rdx
|
||||
popq %rcx
|
||||
popq %rbx
|
||||
ret
|
||||
|
||||
.size _Utf8CountCodePoints, .-_Utf8CountCodePoints
|
||||
|
||||
|
||||
# --- Read-only data (threshold constant) ---
|
||||
.section .rodata
|
||||
.balign 32
|
||||
.Lthreshold_avx:
|
||||
.fill 32, 1, 0xBF
|
||||
.balign 16
|
||||
.Lthreshold_sse:
|
||||
.fill 16, 1, 0xBF
|
||||
|
||||
# --- Mutable data (AVX2 detection cache) ---
|
||||
.section .data
|
||||
.Lavx2_checked:
|
||||
.long 0
|
||||
.Lavx2_flag:
|
||||
.long 0
|
||||
147
runtime/src/main/pascal/blaise_utf8.pas
Normal file
147
runtime/src/main/pascal/blaise_utf8.pas
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
Blaise - An Object Pascal Compiler
|
||||
Copyright (c) 2026 Graeme Geldenhuys
|
||||
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
|
||||
Licensed under the Apache License v2.0 with Runtime Library Exception.
|
||||
See LICENSE file in the project root for full license terms.
|
||||
}
|
||||
|
||||
unit blaise_utf8;
|
||||
|
||||
// UTF-8 code-point counter — SIMD-accelerated (AVX2 with an SSE2 fallback and
|
||||
// a scalar tail), with a one-time CPUID probe for AVX2.
|
||||
//
|
||||
// Inline-assembler port of runtime/src/main/asm/blaise_utf8_x86_64.s — the body
|
||||
// (and its threshold/flag data) move into an `asm … end` routine so the RTL
|
||||
// builds with Blaise's own internal assembler and needs no hand-written .s
|
||||
// (docs/inline-asm-design.adoc, §"Migration of the .s files"). This was the
|
||||
// last .s file; with it gone the RTL is pure Pascal + inline asm.
|
||||
//
|
||||
// Counts UTF-8 code points by counting bytes that are NOT continuation bytes
|
||||
// (a byte is a continuation byte iff its top two bits are 10, i.e. signed value
|
||||
// in [-128,-65]); the SIMD paths compare against the threshold -65 (0xBF).
|
||||
//
|
||||
// System V AMD64: Data -> %rdi, Len -> %esi; result (count) in %eax.
|
||||
|
||||
interface
|
||||
|
||||
function _Utf8CountCodePoints(Data: Pointer; Len: Integer): Integer;
|
||||
|
||||
implementation
|
||||
|
||||
function _Utf8CountCodePoints(Data: Pointer; Len: Integer): Integer;
|
||||
assembler; nostackframe;
|
||||
asm
|
||||
testl %esi, %esi
|
||||
jle .Lreturn_zero
|
||||
movl %esi, %ecx
|
||||
xorl %eax, %eax
|
||||
xorl %edx, %edx
|
||||
cmpl $0, .Lavx2_checked(%rip)
|
||||
jne .Lavx2_known
|
||||
call .Lcheck_avx2
|
||||
xorl %eax, %eax
|
||||
.Lavx2_known:
|
||||
cmpl $0, .Lavx2_flag(%rip)
|
||||
je .Lsse2_entry
|
||||
cmpl $32, %ecx
|
||||
jb .Lsse2_entry
|
||||
vpcmpeqb %ymm2, %ymm2, %ymm2
|
||||
vpsrlw $1, %ymm2, %ymm2
|
||||
vpaddw %ymm2, %ymm2, %ymm1
|
||||
vpcmpeqb %ymm2, %ymm2, %ymm2
|
||||
vpsllw $7, %ymm2, %ymm1
|
||||
vpcmpeqb %ymm1, %ymm1, %ymm1
|
||||
vmovdqa .Lthreshold_avx(%rip), %ymm1
|
||||
vpxor %ymm3, %ymm3, %ymm3
|
||||
.Lavx2_loop:
|
||||
vmovdqu (%rdi, %rdx), %ymm0
|
||||
vpcmpgtb %ymm1, %ymm0, %ymm2
|
||||
vpabsb %ymm2, %ymm2
|
||||
vpaddb %ymm2, %ymm3, %ymm3
|
||||
addl $32, %edx
|
||||
subl $32, %ecx
|
||||
cmpl $32, %ecx
|
||||
jge .Lavx2_loop
|
||||
vpxor %ymm4, %ymm4, %ymm4
|
||||
vpsadbw %ymm4, %ymm3, %ymm3
|
||||
vextracti128 $1, %ymm3, %xmm4
|
||||
vpaddq %xmm4, %xmm3, %xmm3
|
||||
vpshufd $0x0E, %xmm3, %xmm4
|
||||
vpaddq %xmm4, %xmm3, %xmm3
|
||||
vmovd %xmm3, %r8d
|
||||
addl %r8d, %eax
|
||||
vzeroupper
|
||||
jmp .Lsse2_entry
|
||||
.Lsse2_entry:
|
||||
cmpl $16, %ecx
|
||||
jb .Lscalar_entry
|
||||
movdqa .Lthreshold_sse(%rip), %xmm1
|
||||
pxor %xmm3, %xmm3
|
||||
.Lsse2_loop:
|
||||
movdqu (%rdi, %rdx), %xmm0
|
||||
movdqa %xmm0, %xmm2
|
||||
pcmpgtb %xmm1, %xmm2
|
||||
pabsb %xmm2, %xmm2
|
||||
paddb %xmm2, %xmm3
|
||||
addl $16, %edx
|
||||
subl $16, %ecx
|
||||
cmpl $16, %ecx
|
||||
jge .Lsse2_loop
|
||||
pxor %xmm4, %xmm4
|
||||
psadbw %xmm4, %xmm3
|
||||
movd %xmm3, %r8d
|
||||
pshufd $0x0E, %xmm3, %xmm3
|
||||
movd %xmm3, %r9d
|
||||
addl %r8d, %eax
|
||||
addl %r9d, %eax
|
||||
.Lscalar_entry:
|
||||
testl %ecx, %ecx
|
||||
jle .Lreturn
|
||||
.Lscalar_loop:
|
||||
movzbl (%rdi, %rdx), %r8d
|
||||
movl %r8d, %r9d
|
||||
andl $0xC0, %r9d
|
||||
cmpl $0x80, %r9d
|
||||
setne %r9b
|
||||
movzbl %r9b, %r9d
|
||||
addl %r9d, %eax
|
||||
incl %edx
|
||||
decl %ecx
|
||||
jnz .Lscalar_loop
|
||||
.Lreturn:
|
||||
ret
|
||||
.Lreturn_zero:
|
||||
xorl %eax, %eax
|
||||
ret
|
||||
.Lcheck_avx2:
|
||||
pushq %rbx
|
||||
pushq %rcx
|
||||
pushq %rdx
|
||||
movl $7, %eax
|
||||
xorl %ecx, %ecx
|
||||
cpuid
|
||||
testl $0x20, %ebx
|
||||
setne %al
|
||||
movzbl %al, %eax
|
||||
movl %eax, .Lavx2_flag(%rip)
|
||||
movl $1, .Lavx2_checked(%rip)
|
||||
popq %rdx
|
||||
popq %rcx
|
||||
popq %rbx
|
||||
ret
|
||||
.section .rodata
|
||||
.balign 32
|
||||
.Lthreshold_avx:
|
||||
.fill 32, 1, 0xBF
|
||||
.balign 16
|
||||
.Lthreshold_sse:
|
||||
.fill 16, 1, 0xBF
|
||||
.section .data
|
||||
.Lavx2_checked:
|
||||
.long 0
|
||||
.Lavx2_flag:
|
||||
.long 0
|
||||
end;
|
||||
|
||||
end.
|
||||
Loading…
Reference in a new issue