Эта статья про новое расширение ахритектуры трансформеров – Titan от Google –, позволяющее расширить рамки LLM до 2 млн токенов, побудила поинтересоваться, сколько токенов, пригодных для LLM, содержат исходники колоссального софта.
Какой открытый софт будем „препарировать“:
MySQL
VS Code
Blender
Linux*
LLVM*
Итого 5 крупных и известных проектов. Подсчёт происходил на актуальных версиях исходников. Звёздочками отмечен тот софт, кодовая база которого весит больше одного ГБ.
Как будем считать
Сначала скачиваем репозиторий с исходниками, желательно удаляем папку .git или .hg (Firefox использует Mercurial вместо Git), если она есть. Далее перегоняем все исходники в один текстовый файл. Подобным образом кодовую базу обрабатывает сервис GitIngest (их GitHub). Но там есть ограничение на время работы в 20 секунд, чего, коенчно, не хватает для перегонки почти 1,5 ГБ исходников того же ядра, да и написан он на Python. Поэтому для решения этой проблемы необходимо проводить подготовку кодовой базы на своём компьютере с использованием более высокопроизводительного способа. Таким способом стала небольшая многопоточная программа на C++, которую написала китайская LLM DeepSeek — аналог ChatGPT. По завершении работы программы получается текстовый файл prompt.txt со следующей структурой:
Дерево кодовой базы, так как структура кодовой базы является не менее важной информацией, чем её содержимое
Содержимое всех файлов в таком markdown-подобном формате, где начало и конец содержимого файлов обозначается тремя грависами
`:<file_name.file_extension>```\n<file_content>\n```\n\n
Также эта программа выводит число «слов» в кодовой базе — простой подсчёт по разделению кодовой базы по пробелам и переносам строк, это число намного меньше, чем число токенов, подсчитанное продвинутым токенизатором от OpenAI, о котором далее. Эту часть программы следовало бы убрать и сделать это достаточно легко, но можно просто прерывать процесс выполенения в терминале.
Исходники:
Дисклеймер
Автор не умеет писать на C++, весь код сгенерирован LLM и толком не прочитан, только проверена его работоспособность. Хоть в коде и игнорируется папка гита, но тем не менее файлы из неё попадают в итоговый файл, поэтому я и написал, что желательно её удалить, если она есть. Хотя в случае скачивания исходников с GitHub в zip-архиве (кнопка code → download zip) её нет.
Код на C++, который я бегло просмотрел и почти не читал
#include <filesystem> #include <fstream> #include <string> #include <vector> #include <iostream> #include <cstddef> #include <algorithm> #include <cctype> #include <thread> #include <mutex> #include <sstream> #include <atomic> #include <memory> namespace fs = std::filesystem; // Генерация дерева директорий std::string generate_tree(const fs::path& path, const std::string& prefix = "") { std::string tree; std::vector<fs::path> entries; for (const auto& entry : fs::directory_iterator(path)) { if (entry.path().filename() == ".git") continue; entries.push_back(entry.path()); } std::sort(entries.begin(), entries.end(), [](const fs::path& a, const fs::path& b) { return a.filename() < b.filename(); }); for (size_t i = 0; i < entries.size(); ++i) { bool is_last = (i == entries.size() - 1); std::string connector = is_last ? "└── " : "├── "; if (fs::is_directory(entries[i])) { tree += prefix + connector + entries[i].filename().string() + "/\n"; } else { tree += prefix + connector + entries[i].filename().string() + "\n"; } if (fs::is_directory(entries[i])) { std::string new_prefix = prefix + (is_last ? " " : "│ "); tree += generate_tree(entries[i], new_prefix); } } return tree; } // Проверка, является ли файл бинарным bool is_binary(const fs::path& file_path) { try { std::ifstream file(file_path, std::ios::binary); if (!file) return true; char buffer[1024]; while (file.read(buffer, sizeof(buffer))) { for (int i = 0; i < file.gcount(); ++i) { if (static_cast<unsigned char>(buffer[i]) < 32 && buffer[i] != '\n' && buffer[i] != '\r' && buffer[i] != '\t') { return true; } } } return false; } catch (const std::exception& e) { std::cerr << "Error checking binary file " << file_path << ": " << e.what() << '\n'; return true; } } // Обработка файла и добавление его содержимого в output void process_file(const fs::path& file_path, std::ostringstream& oss) { try { std::ifstream file(file_path, std::ios::in); if (!file.is_open()) { std::cerr << "Failed to open file: " << file_path << '\n'; return; } oss << file_path.filename().string() << "```\n"; oss << std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()) << "\n```\n\n"; } catch (const std::exception& e) { std::cerr << "Error processing file " << file_path << ": " << e.what() << '\n'; } } // Обработка группы файлов в одно�� потоке void process_file_chunk(const std::vector<fs::path>& files, std::ostringstream& oss) { for (const auto& file_path : files) { process_file(file_path, oss); } } // Основная функция для многопоточной обработки файлов std::string process_files_multithreaded(const fs::path& path, int num_threads) { std::vector<fs::path> files_to_process; try { for (const auto& entry : fs::recursive_directory_iterator(path)) { if (entry.is_regular_file()) { fs::path file_path = entry.path(); if (file_path.parent_path().filename() == ".git") continue; if (!is_binary(file_path)) { files_to_process.push_back(file_path); } } } } catch (const std::exception& e) { std::cerr << "Error traversing directory " << path << ": " << e.what() << '\n'; } std::sort(files_to_process.begin(), files_to_process.end()); // Разделение файлов на chunks для многопоточной обработки std::vector<std::vector<fs::path>> chunks(num_threads); int chunk_size = files_to_process.size() / num_threads; int remainder = files_to_process.size() % num_threads; int start = 0; for (int i = 0; i < num_threads; ++i) { int current_chunk_size = chunk_size + (i < remainder ? 1 : 0); chunks[i] = std::vector<fs::path>(files_to_process.begin() + start, files_to_process.begin() + start + current_chunk_size); start += current_chunk_size; } // Многопоточная обработка std::vector<std::ostringstream> thread_outputs(num_threads); std::vector<std::thread> threads; for (int i = 0; i < num_threads; ++i) { threads.emplace_back([&, i]() { process_file_chunk(chunks[i], thread_outputs[i]); }); } for (auto& thread : threads) { thread.join(); } // Объединение результатов std::ostringstream final_output; for (auto& oss : thread_outputs) { final_output << oss.str(); } return final_output.str(); } int main(int argc, char* argv[]) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " <path_to_traverse>\n"; return 1; } fs::path path(argv[1]); if (!fs::exists(path) || !fs::is_directory(path)) { std::cerr << "The provided path is not a directory or doesn't exist.\n"; return 1; } // Генерация дерева директорий std::string tree = generate_tree(path); // Обработка файлов с использованием многопоточности int num_threads = std::thread::hardware_concurrency(); if (num_threads <= 0) num_threads = 4; // Fallback to 4 threads if hardware_concurrency() returns 0 std::string output = process_files_multithreaded(path, num_threads); // Объединение дерева и содержимого файлов std::string final_output = tree + '\n' + output; // Запись результата в файл prompt.txt fs::path prompt_file = path.parent_path() / "prompt.txt"; std::ofstream f(prompt_file); if (!f.is_open()) { std::cerr << "Error writing to prompt.txt\n"; return 1; } f.write(final_output.c_str(), final_output.size()); f.close(); std::cout << "prompt.txt has been created at " << prompt_file << '\n'; // Подсчет токенов в prompt.txt std::ifstream infile(prompt_file); if (!infile.is_open()) { std::cerr << "Error reading prompt.txt for token counting\n"; return 1; } std::string content((std::istreambuf_iterator<char>(infile)), std::istreambuf_iterator<char>()); infile.close(); // Упрощенный подсчет токенов (по пробелам) std::size_t token_count = 0; bool in_token = false; for (char c : content) { if (std::isspace(static_cast<unsigned char>(c))) { if (in_token) { ++token_count; in_token = false; } } else { in_token = true; } } if (in_token) ++token_count; std::cout << "Number of tokens in prompt.txt: " << token_count << '\n'; return 0; }
Как код был скомпилирован под Windows в WSL при помощи MinGW64:
x86_64-w64-mingw32-g++ -static-libgcc -static-libstdc++ -o main64.exe cpp.cpp
После подготовки кодовой базы необходимо посчитать токены, для этого будем использовать токенизатор от OpenAI — Tiktoken. Cookbook от OpenAI по тому, как считать токены с помощью Tiktoken, утверждает, что данная библиотека используется в моделях вплоть до GPT-4o. Написан он на Python и Rust, что обеспечивает высокую производительность и быструю токенизацию в совокупности с удобством использования приятного синтаксиса Python. Использовалась кодировка токенизатора o200k_base, которую используют модели GPT-4o и GPT-4o-mini от OpenAI.
Исходники:
Простой код на Python. Почти полностью идентичен коду с OpenAI Cookbook, в том числе на котором, вполне возможно, обучали DeepSeek
import tiktoken def count_tokens(file_path): with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() encoding = tiktoken.encoding_for_model('gpt-4o') # Используется в GPT-4o tokens = encoding.encode(content) return len(tokens) token_count = count_tokens("prompt.txt") print(f"Number of tokens: {token_count}")
Итого, процесс выглядит так:
Скачивание кодовой базы
Её подготовка
Подсчёт токенов
Пример работы этих двух программ при обработке их же исходников
prompt.txt
├── cpp.cpp └── main.py cpp.cpp``` #include <filesystem> #include <fstream> #include <string> #include <vector> #include <iostream> #include <cstddef> #include <algorithm> #include <cctype> #include <thread> #include <mutex> #include <sstream> #include <atomic> #include <memory> namespace fs = std::filesystem; // Генерация дерева директорий std::string generate_tree(const fs::path& path, const std::string& prefix = "") { std::string tree; std::vector<fs::path> entries; for (const auto& entry : fs::directory_iterator(path)) { if (entry.path().filename() == ".git") continue; entries.push_back(entry.path()); } std::sort(entries.begin(), entries.end(), [](const fs::path& a, const fs::path& b) { return a.filename() < b.filename(); }); for (size_t i = 0; i < entries.size(); ++i) { bool is_last = (i == entries.size() - 1); std::string connector = is_last ? "└── " : "├── "; if (fs::is_directory(entries[i])) { tree += prefix + connector + entries[i].filename().string() + "/\n"; } else { tree += prefix + connector + entries[i].filename().string() + "\n"; } if (fs::is_directory(entries[i])) { std::string new_prefix = prefix + (is_last ? " " : "│ "); tree += generate_tree(entries[i], new_prefix); } } return tree; } // Проверка, является ли файл бинарным bool is_binary(const fs::path& file_path) { try { std::ifstream file(file_path, std::ios::binary); if (!file) return true; char buffer[1024]; while (file.read(buffer, sizeof(buffer))) { for (int i = 0; i < file.gcount(); ++i) { if (static_cast<unsigned char>(buffer[i]) < 32 && buffer[i] != '\n' && buffer[i] != '\r' && buffer[i] != '\t') { return true; } } } return false; } catch (const std::exception& e) { std::cerr << "Error checking binary file " << file_path << ": " << e.what() << '\n'; return true; } } // Обработка файла и добавление его содержимого в output void process_file(const fs::path& file_path, std::ostringstream& oss) { try { std::ifstream file(file_path, std::ios::in); if (!file.is_open()) { std::cerr << "Failed to open file: " << file_path << '\n'; return; } oss << file_path.filename().string() << "```\n"; oss << std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()) << "\n```\n\n"; } catch (const std::exception& e) { std::cerr << "Error processing file " << file_path << ": " << e.what() << '\n'; } } // Обработка группы файлов в одном потоке void process_file_chunk(const std::vector<fs::path>& files, std::ostringstream& oss) { for (const auto& file_path : files) { process_file(file_path, oss); } } // Основная функция для многопоточной обработки файлов std::string process_files_multithreaded(const fs::path& path, int num_threads) { std::vector<fs::path> files_to_process; try { for (const auto& entry : fs::recursive_directory_iterator(path)) { if (entry.is_regular_file()) { fs::path file_path = entry.path(); if (file_path.parent_path().filename() == ".git") continue; if (!is_binary(file_path)) { files_to_process.push_back(file_path); } } } } catch (const std::exception& e) { std::cerr << "Error traversing directory " << path << ": " << e.what() << '\n'; } std::sort(files_to_process.begin(), files_to_process.end()); // Разделение файлов на chunks для многопоточной обработки std::vector<std::vector<fs::path>> chunks(num_threads); int chunk_size = files_to_process.size() / num_threads; int remainder = files_to_process.size() % num_threads; int start = 0; for (int i = 0; i < num_threads; ++i) { int current_chunk_size = chunk_size + (i < remainder ? 1 : 0); chunks[i] = std::vector<fs::path>(files_to_process.begin() + start, files_to_process.begin() + start + current_chunk_size); start += current_chunk_size; } // Многопоточная обработка std::vector<std::ostringstream> thread_outputs(num_threads); std::vector<std::thread> threads; for (int i = 0; i < num_threads; ++i) { threads.emplace_back([&, i]() { process_file_chunk(chunks[i], thread_outputs[i]); }); } for (auto& thread : threads) { thread.join(); } // Объединение результатов std::ostringstream final_output; for (auto& oss : thread_outputs) { final_output << oss.str(); } return final_output.str(); } int main(int argc, char* argv[]) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " <path_to_traverse>\n"; return 1; } fs::path path(argv[1]); if (!fs::exists(path) || !fs::is_directory(path)) { std::cerr << "The provided path is not a directory or doesn't exist.\n"; return 1; } // Генерация дерева директорий std::string tree = generate_tree(path); // Обработка файлов с использованием многопоточности int num_threads = std::thread::hardware_concurrency(); if (num_threads <= 0) num_threads = 4; // Fallback to 4 threads if hardware_concurrency() returns 0 std::string output = process_files_multithreaded(path, num_threads); // Объединение дерева и содержимого файлов std::string final_output = tree + '\n' + output; // Запись результата в файл prompt.txt fs::path prompt_file = path.parent_path() / "prompt.txt"; std::ofstream f(prompt_file); if (!f.is_open()) { std::cerr << "Error writing to prompt.txt\n"; return 1; } f.write(final_output.c_str(), final_output.size()); f.close(); std::cout << "prompt.txt has been created at " << prompt_file << '\n'; // Подсчет токенов в prompt.txt std::ifstream infile(prompt_file); if (!infile.is_open()) { std::cerr << "Error reading prompt.txt for token counting\n"; return 1; } std::string content((std::istreambuf_iterator<char>(infile)), std::istreambuf_iterator<char>()); infile.close(); // Упрощенный подсчет токенов (по пробелам) std::size_t token_count = 0; bool in_token = false; for (char c : content) { if (std::isspace(static_cast<unsigned char>(c))) { if (in_token) { ++token_count; in_token = false; } } else { in_token = true; } } if (in_token) ++token_count; std::cout << "Number of tokens in prompt.txt: " << token_count << '\n'; return 0; } ``` main.py``` import tiktoken def count_tokens(file_path): with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() encoding = tiktoken.encoding_for_model('gpt-4o') # Используется в GPT-4 tokens = encoding.encode(content) return len(tokens) token_count = count_tokens("prompt.txt") print(f"Number of tokens: {token_count}") ```
Результат: Number of tokens: 1841. В целом это очень мало токенов, поэтому LLM и справилась с написанием такого рода примитивного, хоть и эффективного, софта.
(Не) Чистота эксперимента
Конечно, организовать структуру файла prompt.txt можно разными способами, что влияет на количество токенов и добавляет/убирает «шумы» — данные, которые к исходному коду напрямую не относятся. Но в любом случае меня интересует скорее порядок и оценка чисел, чем какие-то конкретные значения. Ядро Линукс содержит около 30 млн строк кода, соответственно, — число токенов там огромно, а при подобном подсчёте (организация кодовой базы в файл, в начале которого также находится её дерево, а содержимое каждого файла отделено от содержимого других файлов, и указаны все имена) добавляется около 10 млн строк «шума» и количество строк возрастает до 40 млн кратно количеству файлов в кодовой базе и, соответственно, токенов тоже становится больше, причем повторяющихся токенов, но такая организация как бы позволяет взглянуть на кодовую базу с высоты птичьего полёта — всё как на ладони и даже читабельно для человека. Ещё в кодовую базу могут попадать какие-нибудь не относящиеся к ней файлы и директории вроде .github, .idea, .vscode и прочих. Так что всё на правах for fun.
Результаты
Все вычисления были выполнены за разумное время, исчисляемое минутами, а не часами, что не может не радовать. Однако, изначально планировалось посчитать токены для исходников 11 проектов, но в совокупности со временем загрузки их из интернета и распаковки из архивов это затянулось на долго, даже если не выполнять примитивный подсчёт «токенов» в программе на C++, а только создавать итоговый файл. Возможно, что-то ещё добавится.
MySQL —
242 876 263VS Code —
31 062 093Blender —
82 885 995Linux* —
456 479 607LLVM* —
631 112 839

Выводы
Абсолютно бесполезная, но интересная информация.
