第一次听说 Julia 是 2019 年与其他人关于编程的聊天中,再后来看到 Julia 是在一些比较好的论文中。今天真正试了试,发现 Julia 的确兼顾了 C++ 的运算速度与 Python 的简洁。
我用 Julia, C++, Java, Python 运行了一个多阶段报童模型的动态规划,运算速度如下:
Julia
planning horizon = 40
runtime = 0.6730000972747803 sec
optimal value = 1359.2762353303274
C++
planning horizon is 40 periods
running time of C++ is 0.313397s
Final optimal value is: 1359.28
Java
planning horizon is 40 periods
running time of Java is 1.805 s
final optimal expected value is: 1357.1966898556248
Python
planning horizon = 40
runtime of Python = 40.8762 s
optimal value = 1359.2762353303237
- Julia 的代码量只有 C++ 的 2/3,但是速度差不太多,比 java 还快,比 python 更是快不少。
- Julia 还有交互式编辑器 Pluto,类似 Jupyter notebook,这个用来演示或画动态图很方便
下面是 julia 代码:
Julia
using Statistics
using Distributions
function poisson_pmf(k::Int, λ::Float64)
if k < 0 || λ < 0
return 0.0
elseif k == 0 && λ == 0
return 1.0
end
logp = -λ + k * log(λ) - lgmma(k + 1)
return exp(logp)
end
function poisson_quantile(p::Float64, λ::Float64)
low = 0
high = max(100, Int(3λ))
while low < high
mid = (low + high) ÷ 2
if cdf(Poisson(λ), mid) < p
low = mid + 1
else
high = mid
end
end
return low
end
# --------------------------
# PMF truncation
# --------------------------
function get_pmf_poisson(demands::Vector{Float64}, q::Float64)
T = length(demands)
pmf = Vector{Vector{Tuple{Int, Float64}}}(undef, T)
for t in 1:T
ub = poisson_quantile(q, demands[t])
lb = poisson_quantile(1 - q, demands[t])
support = [(d, pdf(Poisson(demands[t]), d) / (2q - 1))
for d in lb:ub]
pmf[t] = support
end
return pmf
end
# --------------------------
# State
# --------------------------
struct State
period::Int
inventory::Float64
end
Base.:(==)(a::State, b::State) = a.period == b.period && a.inventory == b.inventory
Base.hash(s::State, h::UInt) = hash((s.period, s.inventory), h)
# --------------------------
# Newsvendor DP
# --------------------------
# mutable 表示这个结构体的字段是可以被修改的
mutable struct NewsvendorDP
T::Int
capacity::Float64
stepSize::Float64
fix_cost::Float64
var_cost::Float64
hold_cost::Float64
penalty_cost::Float64
max_I::Float64
min_I::Float64
pmf::Vector{Vector{Tuple{Int, Float64}}}
cache_actions::Dict{State, Float64}
cache_values::Dict{State, Float64}
end
function feasible_actions(model::NewsvendorDP)
Q = Int(model.capacity / model.stepSize)
return [i * model.stepSize for i in 0:Q-1]
end
function transition(model::NewsvendorDP, s::State, a, d)
nextI = s.inventory + a - d
nextI = clamp(nextI, model.min_I, model.max_I)
return State(s.period + 1, nextI)
end
function immediate_cost(model::NewsvendorDP, s::State, a, d)
fix = a > 0 ? model.fix_cost : 0.0
vari = a * model.var_cost
nextI = clamp(s.inventory + a - d, model.min_I, model.max_I)
hold = max(model.hold_cost * nextI, 0.0)
penalty = max(-model.penalty_cost * nextI, 0.0)
return fix + vari + hold + penalty
end
function recursion(model::NewsvendorDP, s::State)
if haskey(model.cache_values, s)
return model.cache_values[s]
end
best_val = Inf
best_q = 0.0
for a in feasible_actions(model)
val = 0.0
for (d, p) in model.pmf[s.period]
val += p * immediate_cost(model, s, a, d)
if s.period < model.T
ns = transition(model, s, a, d)
val += p * recursion(model, ns)
end
end
if val < best_val
best_val = val
best_q = a
end
end
model.cache_actions[s] = best_q
model.cache_values[s] = best_val
return best_val
end
# --------------------------
# main
# --------------------------
function main()
T = 40
mean_demand = 20.0
demands = fill(mean_demand, T)
capacity = 150.0
stepSize = 1.0
fix_cost = 0.0
var_cost = 1.0
hold_cost = 2.0
penalty_cost = 10.0
trunc_q = 0.9999
maxI = 100.0
minI = -100.0
pmf = get_pmf_poisson(demands, trunc_q)
model = NewsvendorDP(
T, capacity, stepSize,
fix_cost, var_cost,
hold_cost, penalty_cost,
maxI, minI, pmf,
Dict(), Dict()
)
s0 = State(1, 0.0)
t0 = time()
val = recursion(model, s0)
t1 = time()
println("planning horizon = $T")
println("runtime = $(t1 - t0) sec")
println("optimal value = $val")
end
main()
对应的C++代码:
cpp
/*
* Created by Zhen Chen on 2025/12/24.
* Email: chen.zhen5526@gmail.com
* Description: vanilla version of DP for newsvendor;
*
*/
#include <array>
#include <boost/functional/hash.hpp>
#include <chrono>
#include <iostream>
#include <limits>
#include <unordered_map>
double poissonCDF(const int k, const double lambda) {
double cumulative = 0.0;
double term = std::exp(-lambda); // P(X=0)
for (int i = 0; i <= k; ++i) {
cumulative += term;
if (i < k)
term *= lambda / (i + 1); // 递推计算 P(X=i)
}
return cumulative;
}
double poissonPMF(const int k, const int lambda) {
if (k < 0 || lambda < 0)
return 0.0; // 确保参数合法
if (k == 0 and lambda == 0)
return 1.0;
// lgamma 对 tgamma 取 ln
const double logP = -lambda + k * std::log(lambda) - std::lgamma(k + 1);
return std::exp(logP); // Use the logarithmic form to avoid overflow from std::tgamma(k + 1)
}
int poissonQuantile(const double p, const double lambda) {
int low = 0, high = std::max(100, static_cast<int>(lambda * 3)); // 初始搜索区间
while (low < high) {
if (const int mid = (low + high) / 2; poissonCDF(mid, lambda) < p) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
std::vector<std::vector<std::array<double, 2>>> getPMFPoisson(const std::vector<double> &demands,
const double truncated_quantile) {
const size_t T = demands.size();
std::vector<int> support_lb(T);
std::vector<int> support_ub(T);
for (size_t i = 0; i < T; ++i) {
support_ub[i] = poissonQuantile(truncated_quantile, demands[i]);
support_lb[i] = poissonQuantile(1 - truncated_quantile, demands[i]);
}
std::vector pmf(T, std::vector<std::array<double, 2>>());
for (int t = 0; t < T; ++t) {
const int demand_length = static_cast<int>((support_ub[t] - support_lb[t] + 1));
pmf[t].resize(demand_length, std::array<double, 2>());
for (int j = 0; j < demand_length; ++j) {
pmf[t][j][0] = support_lb[t] + j;
const int demand = static_cast<int>(pmf[t][j][0]);
pmf[t][j][1] =
poissonPMF(demand, static_cast<int>(demands[t])) / (2 * truncated_quantile - 1);
}
}
return pmf;
}
class State {
int period{}; // c++11, {} 值初始化,默认为 0
double ini_inventory{};
public:
State() {}
explicit State(const int period, const double ini_inventory)
: period(period), ini_inventory(ini_inventory) {};
[[nodiscard]] double get_ini_inventory() const { return ini_inventory; }
[[nodiscard]] int getPeriod() const { return period; }
// for unordered map
bool operator==(const State &other) const {
return period == other.period && ini_inventory == other.ini_inventory;
}
friend struct std::hash<State>;
// for ordered map
bool operator<(const State &other) const {
if (period != other.period)
return period < other.period;
if (ini_inventory != other.ini_inventory)
return ini_inventory < other.ini_inventory;
return false;
}
friend std::ostream &operator<<(std::ostream &os, const State &state);
};
template <> struct std::hash<State> {
// size_t 表示无符号整数
size_t operator()(const State &s) const noexcept {
// noexcept 表示这个函数不会抛出异常
// boost 的哈希计算更安全
std::size_t seed = 0;
boost::hash_combine(seed, s.period);
boost::hash_combine(seed, s.ini_inventory);
return seed;
}
};
class NewsvendorDP {
size_t T;
double capacity;
double fix_order_cost;
double unit_vari_order_cost;
double unit_hold_cost;
double unit_penalty_cost;
double truncated_quantile;
double max_I;
double min_I;
std::vector<std::vector<std::array<double, 2>>> pmf;
bool parallel{};
bool compute_Gy = false;
State ini_state{};
std::unordered_map<State, double> cache_actions;
std::unordered_map<State, double> cache_values;
public:
NewsvendorDP(const size_t T, const double capacity, const double fix_order_cost,
const double unit_vari_order_cost, const double unit_hold_cost,
const double unit_penalty_cost, const double truncated_quantile, const double max_I,
const double min_I, const std::vector<std::vector<std::array<double, 2>>> &pmf)
: T(static_cast<int>(T)), capacity(capacity), fix_order_cost(fix_order_cost),
unit_vari_order_cost(unit_vari_order_cost), unit_hold_cost(unit_hold_cost),
unit_penalty_cost(unit_penalty_cost), truncated_quantile(truncated_quantile), max_I(max_I),
min_I(min_I), pmf(pmf) {};
[[nodiscard]] std::vector<double> feasibleActions() const {
const int QNum = static_cast<int>(capacity);
std::vector<double> actions(QNum);
for (int i = 0; i < QNum; i = i + 1) {
actions[i] = i;
}
return actions;
}
[[nodiscard]] State stateTransitionFunction(const State &state, const double action,
const double demand) const {
double nextInventory = state.get_ini_inventory() + action - demand;
nextInventory = nextInventory > max_I ? max_I : nextInventory;
nextInventory = nextInventory < min_I ? min_I : nextInventory;
const int nextPeriod = state.getPeriod() + 1;
// C++11 引入了统一的列表初始化(Uniform Initialization),鼓励使用大括号 {} 初始化类
const auto newState = State{nextPeriod, nextInventory};
return newState;
}
[[nodiscard]] double immediateValueFunction(const State &state, const double action,
const double demand) const {
const double fixCost = action > 0 ? fix_order_cost : 0;
const double variCost = action * unit_vari_order_cost;
double nextInventory = state.get_ini_inventory() + action - demand;
nextInventory = nextInventory > max_I ? max_I : nextInventory;
nextInventory = nextInventory < min_I ? min_I : nextInventory;
const double holdCost = std::max(unit_hold_cost * nextInventory, 0.0);
const double penaltyCost = std::max(-unit_penalty_cost * nextInventory, 0.0);
const double totalCost = fixCost + variCost + holdCost + penaltyCost;
return totalCost;
}
double recursion(const State &state) { // NOLINT
double bestQ = 0.0;
double bestValue = std::numeric_limits<double>::max();
const std::vector<double> actions = feasibleActions(); // should not move inside
for (const double action : actions) {
double thisValue = 0;
for (auto demandAndProb : pmf[state.getPeriod() - 1]) {
thisValue += demandAndProb[1] * immediateValueFunction(state, action, demandAndProb[0]);
if (state.getPeriod() < T) {
auto newState = stateTransitionFunction(state, action, demandAndProb[0]);
auto it = cache_values.find(newState);
if (it != cache_values.end()) {
thisValue += demandAndProb[1] * it->second;
} else {
thisValue += demandAndProb[1] * recursion(newState);
}
}
}
if (thisValue < bestValue) {
bestValue = thisValue;
bestQ = action;
}
}
cache_actions[state] = bestQ;
cache_values[state] = bestValue;
return bestValue;
}
};
int main() {
constexpr int T = 40;
constexpr double mean_demand = 20;
const std::vector demands(T, mean_demand);
constexpr double capacity = 150; // maximum ordering quantity
constexpr double fix_order_cost = 0;
constexpr double unitVariOderCost = 1;
constexpr double unit_hold_cost = 2;
constexpr double unit_penalty_cost = 10;
constexpr double truncQuantile = 0.9999; // truncated quantile for the demand distribution
constexpr double maxI = 100; // maximum possible inventory
constexpr double minI = -100; // minimum possible inventory
const auto pmf = getPMFPoisson(demands, truncQuantile);
auto model = NewsvendorDP(T, capacity, fix_order_cost, unitVariOderCost, unit_hold_cost,
unit_penalty_cost, truncQuantile, maxI, minI, pmf);
const auto initialState = State(1, 0);
const auto start_time = std::chrono::high_resolution_clock::now();
const auto optValue = model.recursion(initialState);
const auto end_time = std::chrono::high_resolution_clock::now();
const std::chrono::duration<double> duration = end_time - start_time;
std::cout << "planning horizon is " << T << " periods" << std::endl;
std::cout << "running time of C++ is " << duration << std::endl;
std::cout << "Final optimal value is: " << optValue << std::endl;
return 0;
}