init: raw cpuid/rdtsc access via ctypes

This commit is contained in:
ImSavsis 2026-07-19 23:29:46 +05:00
commit fa82965ab5
15 changed files with 437 additions and 0 deletions

27
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: ci
on:
push:
branches: [master]
jobs:
test:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: ilammy/msvc-dev-cmd@v1
- run: cl /LD /EHsc /Fe:src\rawcpu\rawcpu.dll native\rawcpu.cpp
- run: pip install -e .[dev]
- run: pytest --cov=rawcpu --cov-report=xml
- uses: codecov/codecov-action@v4
if: matrix.python-version == '3.12'
with:
files: ./coverage.xml
- run: python -m build
- run: python -m twine check dist/*

23
.github/workflows/publish.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: publish
on:
release:
types: [published]
permissions:
id-token: write
jobs:
publish:
runs-on: windows-latest
environment: pypi
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: ilammy/msvc-dev-cmd@v1
- run: cl /LD /EHsc /Fe:src\rawcpu\rawcpu.dll native\rawcpu.cpp
- run: pip install build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
__pycache__/
*.pyc
.pytest_cache/
dist/
build/
*.egg-info/
.claude/
.coverage
coverage.xml
*.obj
*.exp
*.lib
src/rawcpu/rawcpu.dll

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Savsis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

59
README.md Normal file
View file

@ -0,0 +1,59 @@
# rawcpu
[![CI](https://github.com/ImSavsis/rawcpu/actions/workflows/ci.yml/badge.svg)](https://github.com/ImSavsis/rawcpu/actions/workflows/ci.yml)
[![Coverage](https://codecov.io/gh/ImSavsis/rawcpu/branch/master/graph/badge.svg)](https://codecov.io/gh/ImSavsis/rawcpu)
[![pypi](https://img.shields.io/pypi/v/rawcpu.svg)](https://pypi.org/project/rawcpu/)
[![downloads](https://img.shields.io/pypi/dm/rawcpu.svg)](https://pypi.org/project/rawcpu/)
[![versions](https://img.shields.io/pypi/pyversions/rawcpu.svg)](https://pypi.org/project/rawcpu/)
[![license](https://img.shields.io/github/license/ImSavsis/rawcpu.svg)](https://github.com/ImSavsis/rawcpu/blob/master/LICENSE)
прямой доступ к инструкциям `CPUID`/`RDTSC` из питона — то, что нормально делает только C++, а чистый питон вообще никак. крошечная нативная dll под капотом + ctypes сверху, никакого Python C API.
```mermaid
sequenceDiagram
App->>rawcpu: cpuid(1) / rdtsc()
rawcpu->>DLL: ctypes call
DLL->>CPU: инструкция CPUID/RDTSC напрямую
CPU-->>DLL: сырые регистры / счётчик тактов
DLL-->>App: tuple(int) / int
```
## установка
```
pip install rawcpu
```
пока только windows x64.
## юзать
```python
import rawcpu
print(rawcpu.vendor()) # 'GenuineIntel' / 'AuthenticAMD'
print(rawcpu.brand()) # 'AMD Ryzen 9 ...'
print(rawcpu.has_feature("avx2")) # True/False, без запуска кода который может упасть на старом CPU
start = rawcpu.rdtsc()
# твой код
end = rawcpu.rdtsc()
print(end - start, "тактов") # точнее чем time.perf_counter(), считает реальные такты CPU
```
## документация
- [quickstart](docs/quickstart.md)
- [api](docs/api.md)
## фичи
- сырой `cpuid(leaf, subleaf)` — регистры eax/ebx/ecx/edx как есть
- `rdtsc()` — счётчик тактов CPU напрямую, без ОС-абстракции
- `has_feature()` — проверка sse/avx/avx2/aes/fma и т.д. по битам без падений на неподдерживающем CPU
- typed, питон 3.10–3.13
## чего нет
- linux/mac — только windows пока
- запись в регистры / что-то в духе MSR — это уже ring 0, юзермодом не достать без драйвера

25
docs/api.md Normal file
View file

@ -0,0 +1,25 @@
# api
## `cpuid(leaf: int, subleaf: int = 0) -> tuple[int, int, int, int]`
сырой вызов инструкции `CPUID`, возвращает `(eax, ebx, ecx, edx)`.
## `rdtsc() -> int`
сырое значение счётчика тактов процессора (инструкция `RDTSC`), монотонно растёт.
## `vendor() -> str`
строка вендора CPU, например `GenuineIntel` или `AuthenticAMD`.
## `brand() -> str`
полная строка модели процессора из расширенных leaf'ов CPUID.
## `has_feature(name: str) -> bool`
проверка бита фичи в CPUID без риска упасть на CPU без её поддержки. кидает `ValueError` если имя не из `FEATURES`.
## `FEATURES: dict[str, tuple[int, str, int]]`
словарь `имя -> (leaf, регистр, номер бита)`. поддерживаются: `sse`, `sse2`, `sse3`, `ssse3`, `fma`, `sse4_1`, `sse4_2`, `popcnt`, `aes`, `avx`, `rdrnd`, `avx2`, `avx512f`.

48
docs/quickstart.md Normal file
View file

@ -0,0 +1,48 @@
# quickstart
## установка
```
pip install rawcpu
```
## узнать процессор
```python
import rawcpu
print(rawcpu.vendor()) # GenuineIntel / AuthenticAMD
print(rawcpu.brand()) # полная строка модели
```
## проверить фичи перед SIMD-кодом
```python
if rawcpu.has_feature("avx2"):
# можно грузить AVX2-путь
...
else:
# fallback
...
```
список доступных имён — `rawcpu.FEATURES`.
## точный замер тактов
`time.perf_counter()` меряет время через ОС, `rdtsc()` — реальные такты процессора напрямую:
```python
start = rawcpu.rdtsc()
do_something()
end = rawcpu.rdtsc()
print(end - start, "cycles")
```
## сырой cpuid
```python
eax, ebx, ecx, edx = rawcpu.cpuid(leaf=7, subleaf=0)
```
leaf/subleaf — как в интеловской документации по CPUID.

18
llms.txt Normal file
View file

@ -0,0 +1,18 @@
# rawcpu
> Raw CPUID and RDTSC access for Python via a tiny native DLL wrapped with ctypes. Exposes CPU feature detection (SSE/AVX/AES/etc.) and cycle-accurate timing that plain Python cannot do natively.
Windows x64 only for now. No Python C API used — a small MSVC-compiled DLL exports two functions (raw_cpuid, raw_rdtsc) loaded via ctypes.WinDLL, so it works across Python 3.10-3.13 without per-version ABI builds.
## Docs
- [README](https://github.com/ImSavsis/rawcpu/blob/master/README.md): install, usage, feature list
- [docs/quickstart.md](https://github.com/ImSavsis/rawcpu/blob/master/docs/quickstart.md)
- [docs/api.md](https://github.com/ImSavsis/rawcpu/blob/master/docs/api.md)
- [native/rawcpu.cpp](https://github.com/ImSavsis/rawcpu/blob/master/native/rawcpu.cpp): the native DLL source
- [src/rawcpu/__init__.py](https://github.com/ImSavsis/rawcpu/blob/master/src/rawcpu/__init__.py): high-level API and CPU feature bit table
## Notes
- No MSR/register-write access — that requires ring 0, not reachable from usermode without a kernel driver
- Linux/macOS not supported yet

18
native/rawcpu.cpp Normal file
View file

@ -0,0 +1,18 @@
#include <intrin.h>
extern "C" {
__declspec(dllexport) void raw_cpuid(int leaf, int subleaf, unsigned int* out) {
int regs[4];
__cpuidex(regs, leaf, subleaf);
out[0] = static_cast<unsigned int>(regs[0]);
out[1] = static_cast<unsigned int>(regs[1]);
out[2] = static_cast<unsigned int>(regs[2]);
out[3] = static_cast<unsigned int>(regs[3]);
}
__declspec(dllexport) unsigned long long raw_rdtsc() {
return __rdtsc();
}
}

35
pyproject.toml Normal file
View file

@ -0,0 +1,35 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "rawcpu"
version = "0.1.0"
description = "raw CPUID and RDTSC access for Python via a tiny native DLL — CPU feature detection and cycle-accurate timing"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [{ name = "Savsis" }]
keywords = ["cpuid", "rdtsc", "cpu", "low-level", "simd", "intrinsics"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: MIT License",
"Operating System :: Microsoft :: Windows",
]
[project.urls]
Homepage = "https://github.com/ImSavsis/rawcpu"
Issues = "https://github.com/ImSavsis/rawcpu/issues"
[project.optional-dependencies]
dev = ["pytest", "pytest-cov", "build", "twine"]
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
rawcpu = ["*.dll", "py.typed"]

28
setup.py Normal file
View file

@ -0,0 +1,28 @@
from setuptools import setup
from setuptools.dist import Distribution
try:
from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
class bdist_wheel(_bdist_wheel):
def finalize_options(self):
_bdist_wheel.finalize_options(self)
self.root_is_pure = False
def get_tag(self):
_python, _abi, plat = _bdist_wheel.get_tag(self)
return "py3", "none", plat
except ImportError:
bdist_wheel = None
class BinaryDistribution(Distribution):
def has_ext_modules(self):
return True
setup(
distclass=BinaryDistribution,
cmdclass={"bdist_wheel": bdist_wheel} if bdist_wheel else {},
)

69
src/rawcpu/__init__.py Normal file
View file

@ -0,0 +1,69 @@
from ._native import cpuid_raw, rdtsc_raw
__version__ = "0.1.0"
def cpuid(leaf: int, subleaf: int = 0):
return cpuid_raw(leaf, subleaf)
def rdtsc() -> int:
return rdtsc_raw()
def _regs_to_str(*regs: int) -> str:
out = bytearray()
for r in regs:
out += r.to_bytes(4, "little")
return out.decode("ascii", errors="replace")
def vendor() -> str:
_eax, ebx, ecx, edx = cpuid(0)
return _regs_to_str(ebx, edx, ecx)
def brand() -> str:
parts = []
for leaf in (0x80000002, 0x80000003, 0x80000004):
eax, ebx, ecx, edx = cpuid(leaf)
parts.append(_regs_to_str(eax, ebx, ecx, edx))
return "".join(parts).strip("\x00").strip()
FEATURES = {
"sse": (1, "edx", 25),
"sse2": (1, "edx", 26),
"sse3": (1, "ecx", 0),
"ssse3": (1, "ecx", 9),
"fma": (1, "ecx", 12),
"sse4_1": (1, "ecx", 19),
"sse4_2": (1, "ecx", 20),
"popcnt": (1, "ecx", 23),
"aes": (1, "ecx", 25),
"avx": (1, "ecx", 28),
"rdrnd": (1, "ecx", 30),
"avx2": (7, "ebx", 5),
"avx512f": (7, "ebx", 16),
}
_REG_INDEX = {"eax": 0, "ebx": 1, "ecx": 2, "edx": 3}
def has_feature(name: str) -> bool:
if name not in FEATURES:
raise ValueError(f"unknown feature {name!r}, see rawcpu.FEATURES")
leaf, reg, bit = FEATURES[name]
regs = cpuid(leaf)
value = regs[_REG_INDEX[reg]]
return bool(value & (1 << bit))
__all__ = [
"cpuid",
"rdtsc",
"vendor",
"brand",
"has_feature",
"FEATURES",
]

21
src/rawcpu/_native.py Normal file
View file

@ -0,0 +1,21 @@
import ctypes
import os
_dll_path = os.path.join(os.path.dirname(__file__), "rawcpu.dll")
_lib = ctypes.WinDLL(_dll_path)
_lib.raw_cpuid.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_uint32)]
_lib.raw_cpuid.restype = None
_lib.raw_rdtsc.argtypes = []
_lib.raw_rdtsc.restype = ctypes.c_uint64
def cpuid_raw(leaf: int, subleaf: int = 0):
out = (ctypes.c_uint32 * 4)()
_lib.raw_cpuid(leaf, subleaf, out)
return tuple(out)
def rdtsc_raw() -> int:
return _lib.raw_rdtsc()

0
src/rawcpu/py.typed Normal file
View file

32
tests/test_rawcpu.py Normal file
View file

@ -0,0 +1,32 @@
import rawcpu
def test_vendor_not_empty():
assert len(rawcpu.vendor()) > 0
def test_brand_not_empty():
assert len(rawcpu.brand()) > 0
def test_rdtsc_moves_forward():
a = rawcpu.rdtsc()
b = rawcpu.rdtsc()
assert b >= a
def test_cpuid_leaf_zero_reports_max_leaf():
eax, _ebx, _ecx, _edx = rawcpu.cpuid(0)
assert eax >= 1
def test_has_feature_sse2_true_on_any_modern_x86():
assert rawcpu.has_feature("sse2") is True
def test_has_feature_unknown_raises():
try:
rawcpu.has_feature("not-a-real-feature")
assert False, "expected ValueError"
except ValueError:
pass