init: raw cpuid/rdtsc access via ctypes
This commit is contained in:
commit
fa82965ab5
15 changed files with 437 additions and 0 deletions
27
.github/workflows/ci.yml
vendored
Normal file
27
.github/workflows/ci.yml
vendored
Normal 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
23
.github/workflows/publish.yml
vendored
Normal 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
13
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
59
README.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# rawcpu
|
||||
|
||||
[](https://github.com/ImSavsis/rawcpu/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/ImSavsis/rawcpu)
|
||||
[](https://pypi.org/project/rawcpu/)
|
||||
[](https://pypi.org/project/rawcpu/)
|
||||
[](https://pypi.org/project/rawcpu/)
|
||||
[](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
25
docs/api.md
Normal 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
48
docs/quickstart.md
Normal 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
18
llms.txt
Normal 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
18
native/rawcpu.cpp
Normal 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
35
pyproject.toml
Normal 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
28
setup.py
Normal 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
69
src/rawcpu/__init__.py
Normal 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
21
src/rawcpu/_native.py
Normal 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
0
src/rawcpu/py.typed
Normal file
32
tests/test_rawcpu.py
Normal file
32
tests/test_rawcpu.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue