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