Refactor civilization flow reward calculations for improved accuracy and performance

This commit is contained in:
Kirill
2026-10-08 21:25:39 +03:00
parent c42940369a
commit 459fbb2a4d
3 changed files with 683 additions and 0 deletions
@@ -441,3 +441,20 @@ godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -
Пять полных циклов в секунду пока не достигнуты: тяжёлый цикл составляет 423.0 мс; до 200 мс нужно ещё около 2.11-кратного ускорения. Следующая крупная статья — подготовка потоков цивилизаций, около 115.2 мс/цикл. Пять полных циклов в секунду пока не достигнуты: тяжёлый цикл составляет 423.0 мс; до 200 мс нужно ещё около 2.11-кратного ускорения. Следующая крупная статья — подготовка потоков цивилизаций, около 115.2 мс/цикл.
Полные результаты и контрольные суммы: `physical_totals_step2_results.json`. Базовая версия: `9357f757a95db96274d4e115c7e498b58eca6902`. Сохранение игрока не использовалось. Полные результаты и контрольные суммы: `physical_totals_step2_results.json`. Базовая версия: `9357f757a95db96274d4e115c7e498b58eca6902`. Сохранение игрока не использовалось.
## Упрощение расчёта наград потоков цивилизаций — 2026-10-08
Третий этап убирает промежуточное динамическое число при умножении точного целого населения на float-ставку награды. Частное и дробный остаток вычисляются напрямую, а дробный перенос сохраняет прежнее округление. Это применяется к наградам войны, урона и колонизации; размеры населения остаются точными целыми.
Сравнение с коммитом `c429403` (после второго этапа): те же два диагностических сохранения, скорость 6, интервал 0,2 секунды, CPU-профиль включён. Порядок пар — до → после → после → до; два прогона по 20 циклов на версию и сохранение. Каждый прогон обработал 400 000 игровых лет, валидация сохранений прошла, SHA-256 входов совпали. Сохранение игрока не использовалось.
| Сохранение | До, мс | После, мс | Сокращение времени | Полных циклов/с после |
|---|---:|---:|---:|---:|
| Все объекты | 380,579 | 357,026 | 6,19% | 2,801 |
| Много объектов и партий | 432,912 | 409,268 | 5,46% | 2,443 |
Подготовка потоков уменьшилась с 111,980 до 87,832 мс на сохранении со всеми объектами и со 117,004 до 92,100 мс на тяжёлом сохранении — примерно на 21% в обоих случаях. Остальные основные участки остались в пределах погрешности замера.
Полные состояния до и после совпали после пяти последовательных интервалов на каждом сохранении (включая дробные интервалы). Тест оптимизаций прошёл: 1889 проверок без ошибок. Цель пяти полных циклов в секунду ещё не достигнута: тяжёлому циклу требуется ускорение примерно в 2,05 раза, чтобы уложиться в 200 мс.
Полные профили и SHA-256 входов: `civilization_flow_prepare_step3_results.json`.
@@ -0,0 +1,645 @@
{
"reference_commit": "c429403",
"delta_seconds": 0.2,
"exact_state_differential": true,
"input_sha256": {
"all_objects_speed6": "44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67",
"many_objects_fragmented_stars_cycle": "1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a"
},
"order": "before-after-after-before",
"runs": [
{
"cycles": 20,
"max_ms": 417.124,
"mean_ms": 381.20745,
"min_ms": 328.622,
"model": "before",
"profile": {
"assign_homes": [
24482,
80
],
"civilization_flow_capacity": [
127513,
20
],
"civilization_flow_colonies": [
54148,
20
],
"civilization_flow_finish": [
140918,
20
],
"civilization_flow_ledger": [
905665,
20
],
"civilization_flow_plan": [
2240897,
20
],
"civilization_operator_build": [
63936,
20
],
"civilization_operator_solve": [
685115,
860
],
"enforce_capacity": [
323853,
80
],
"generation_products": [
195550,
40
],
"generation_withdrawal": [
565054,
60
],
"generations": [
2802836,
20
],
"initialize": [
117049,
20
],
"newborn_batches": [
123941,
3016
],
"physical_amounts_build": [
294599,
200
],
"production": [
385593,
20
],
"trends": [
363466,
20
]
},
"run": 1,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 390.389,
"mean_ms": 357.03875,
"min_ms": 302.117,
"model": "after",
"profile": {
"assign_homes": [
24490,
80
],
"civilization_flow_capacity": [
125647,
20
],
"civilization_flow_colonies": [
54325,
20
],
"civilization_flow_finish": [
141339,
20
],
"civilization_flow_ledger": [
902399,
20
],
"civilization_flow_plan": [
1755403,
20
],
"civilization_operator_build": [
63055,
20
],
"civilization_operator_solve": [
683933,
860
],
"enforce_capacity": [
324284,
80
],
"generation_products": [
197296,
40
],
"generation_withdrawal": [
562515,
60
],
"generations": [
2804447,
20
],
"initialize": [
117904,
20
],
"newborn_batches": [
123892,
3016
],
"physical_amounts_build": [
296839,
200
],
"production": [
389361,
20
],
"trends": [
364246,
20
]
},
"run": 1,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 390.735,
"mean_ms": 357.0124,
"min_ms": 304.896,
"model": "after",
"profile": {
"assign_homes": [
24460,
80
],
"civilization_flow_capacity": [
125447,
20
],
"civilization_flow_colonies": [
54216,
20
],
"civilization_flow_finish": [
139778,
20
],
"civilization_flow_ledger": [
906760,
20
],
"civilization_flow_plan": [
1757858,
20
],
"civilization_operator_build": [
63411,
20
],
"civilization_operator_solve": [
687170,
860
],
"enforce_capacity": [
325360,
80
],
"generation_products": [
196450,
40
],
"generation_withdrawal": [
558924,
60
],
"generations": [
2799057,
20
],
"initialize": [
117825,
20
],
"newborn_batches": [
123788,
3016
],
"physical_amounts_build": [
295498,
200
],
"production": [
385114,
20
],
"trends": [
367268,
20
]
},
"run": 2,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 410.865,
"mean_ms": 379.9508,
"min_ms": 328.074,
"model": "before",
"profile": {
"assign_homes": [
24504,
80
],
"civilization_flow_capacity": [
125523,
20
],
"civilization_flow_colonies": [
54072,
20
],
"civilization_flow_finish": [
138884,
20
],
"civilization_flow_ledger": [
900719,
20
],
"civilization_flow_plan": [
2238292,
20
],
"civilization_operator_build": [
63477,
20
],
"civilization_operator_solve": [
687863,
860
],
"enforce_capacity": [
323929,
80
],
"generation_products": [
196146,
40
],
"generation_withdrawal": [
557728,
60
],
"generations": [
2793469,
20
],
"initialize": [
117168,
20
],
"newborn_batches": [
123830,
3016
],
"physical_amounts_build": [
294159,
200
],
"production": [
382811,
20
],
"trends": [
366676,
20
]
},
"run": 2,
"save": "all_objects_speed6"
},
{
"cycles": 20,
"max_ms": 454.17,
"mean_ms": 431.32255,
"min_ms": 380.165,
"model": "before",
"profile": {
"assign_homes": [
18277,
60
],
"civilization_flow_capacity": [
138886,
20
],
"civilization_flow_colonies": [
239194,
20
],
"civilization_flow_finish": [
140538,
20
],
"civilization_flow_ledger": [
1823769,
20
],
"civilization_flow_plan": [
2332907,
20
],
"civilization_operator_build": [
63920,
20
],
"civilization_operator_solve": [
689691,
860
],
"enforce_capacity": [
244580,
60
],
"generation_products": [
80081,
20
],
"generation_withdrawal": [
823351,
40
],
"generations": [
2417052,
20
],
"initialize": [
145160,
20
],
"newborn_batches": [
55841,
1490
],
"physical_amounts_build": [
364671,
140
],
"production": [
414028,
20
],
"trends": [
365376,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle"
},
{
"cycles": 20,
"max_ms": 437.315,
"mean_ms": 409.44945,
"min_ms": 360.224,
"model": "after",
"profile": {
"assign_homes": [
18338,
60
],
"civilization_flow_capacity": [
139951,
20
],
"civilization_flow_colonies": [
238906,
20
],
"civilization_flow_finish": [
141435,
20
],
"civilization_flow_ledger": [
1834486,
20
],
"civilization_flow_plan": [
1841420,
20
],
"civilization_operator_build": [
64315,
20
],
"civilization_operator_solve": [
691338,
860
],
"enforce_capacity": [
245861,
60
],
"generation_products": [
79738,
20
],
"generation_withdrawal": [
825947,
40
],
"generations": [
2423500,
20
],
"initialize": [
145491,
20
],
"newborn_batches": [
56085,
1490
],
"physical_amounts_build": [
371874,
140
],
"production": [
441338,
20
],
"trends": [
366919,
20
]
},
"run": 1,
"save": "many_objects_fragmented_stars_cycle"
},
{
"cycles": 20,
"max_ms": 424.959,
"mean_ms": 409.08685,
"min_ms": 359.281,
"model": "after",
"profile": {
"assign_homes": [
18404,
60
],
"civilization_flow_capacity": [
140108,
20
],
"civilization_flow_colonies": [
239591,
20
],
"civilization_flow_finish": [
141361,
20
],
"civilization_flow_ledger": [
1836691,
20
],
"civilization_flow_plan": [
1842577,
20
],
"civilization_operator_build": [
64001,
20
],
"civilization_operator_solve": [
692117,
860
],
"enforce_capacity": [
245631,
60
],
"generation_products": [
80486,
20
],
"generation_withdrawal": [
829783,
40
],
"generations": [
2438245,
20
],
"initialize": [
145228,
20
],
"newborn_batches": [
56248,
1490
],
"physical_amounts_build": [
369443,
140
],
"production": [
416359,
20
],
"trends": [
367683,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle"
},
{
"cycles": 20,
"max_ms": 452.639,
"mean_ms": 434.50095,
"min_ms": 387.05,
"model": "before",
"profile": {
"assign_homes": [
18271,
60
],
"civilization_flow_capacity": [
140778,
20
],
"civilization_flow_colonies": [
239800,
20
],
"civilization_flow_finish": [
141233,
20
],
"civilization_flow_ledger": [
1836252,
20
],
"civilization_flow_plan": [
2347237,
20
],
"civilization_operator_build": [
64454,
20
],
"civilization_operator_solve": [
693176,
860
],
"enforce_capacity": [
244735,
60
],
"generation_products": [
80563,
20
],
"generation_withdrawal": [
830888,
40
],
"generations": [
2436028,
20
],
"initialize": [
145261,
20
],
"newborn_batches": [
56321,
1490
],
"physical_amounts_build": [
369324,
140
],
"production": [
417541,
20
],
"trends": [
369444,
20
]
},
"run": 2,
"save": "many_objects_fragmented_stars_cycle"
}
],
"speed": 6
}
+21
View File
@@ -303,6 +303,27 @@ static func split_arrays(remainders: Dictionary, key: String, count: Variant, id
static func accumulate_rate(remainders: Dictionary, key: String, count: Variant, probability: Variant) -> Variant: static func accumulate_rate(remainders: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
# Reward rates are floats while populations are exact whole values. Keep
# the product as quotient plus fractional carry instead of constructing an
# intermediate Exact Value and flooring it again.
if probability is float and is_finite(probability) and probability >= 0.0 and (count is int or (count is E.Value and count.fraction == 0.0)) and E.ge(count, 0):
var terms: Array = E.parts(probability)
if terms[2] > 0:
var population: Array = E._blocks(count)[0]
var whole_product: Array = E.Big.multiply(population, terms[0])
var qr: Array = E.Big.divmod(whole_product, terms[1])
var fractional: float = E._fraction_ratio(qr[1], terms[1])
var carry: float = E.approx(remainders.get(key, 0))
if carry >= 0.0 and carry < 1.0:
if qr[1] == [0]:
remainders[key] = carry
return E._compose(qr[0])
fractional += carry
if fractional > 0.5 and absf(fractional - roundf(fractional)) <= 1e-12:
fractional = roundf(fractional)
var extra: int = floori(fractional)
remainders[key] = fractional - extra
return E.add(E._compose(qr[0]), extra)
var product: Variant = E.multiply(count, E.maximum(0, probability)) var product: Variant = E.multiply(count, E.maximum(0, probability))
var carry: float = E.approx(remainders.get(key, 0)) var carry: float = E.approx(remainders.get(key, 0))
if carry >= 0.0 and carry < 1.0 and E.ge(product, 0): if carry >= 0.0 and carry < 1.0 and E.ge(product, 0):