Skip to content

Commit b394eee

Browse files
authored
Avoid underflow and overflow in norm() (#975)
* Avoid underflow in norm() * Perform return-type assertion at last step * Make _init_zero() take AbstractArray * Define maxabs_nested() to find maximum of absolute values of entries in nested array * Remove _norm() and define norm() directly for type stability * Use maxabs_nested() to define norm() stably without underflow * Call _init_zero() without qualification by StaticArrays * Define norm_scaled() and call it inside norm() * norm(a,p) does not call norm_scaled(a,p) if p == 0, 2, Inf * Make code more succinct * Use normInf() instead of maxabs_nested() * Rename aₘ to scale * Calculate scale with mapreduce() * Re-introduce maxabs_nested to avoid type instability * norm() calls _norm() and _norm_scaled() * _norm_scaled(Size(a), a, p) does not need to deal with p==2 * Add test for overflow * Add tests for underflow and overflow for p ≥ 2 * Avoid using iszero(l) and isinf(l) for speedup * Avoid using iszero(scale) for speedup
1 parent 0aefabe commit b394eee

File tree

2 files changed

+67
-20
lines changed

2 files changed

+67
-20
lines changed

src/linalg.jl

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -219,17 +219,42 @@ end
219219
# Norms
220220
_inner_eltype(v::AbstractArray) = isempty(v) ? eltype(v) : _inner_eltype(first(v))
221221
_inner_eltype(x::Number) = typeof(x)
222-
@inline _init_zero(v::StaticArray) = float(norm(zero(_inner_eltype(v))))
222+
@inline _init_zero(v::AbstractArray) = float(norm(zero(_inner_eltype(v))))
223223

224224
@inline function LinearAlgebra.norm_sqr(v::StaticArray)
225225
return mapreduce(LinearAlgebra.norm_sqr, +, v; init=_init_zero(v))
226226
end
227227

228+
@inline maxabs_nested(a::Number) = abs(a)
229+
function maxabs_nested(a::AbstractArray)
230+
prod(size(a)) == 0 && (return _init_zero(a))
231+
232+
m = maxabs_nested(a[1])
233+
for j = 2:prod(size(a))
234+
m = @fastmath max(m, maxabs_nested(a[j]))
235+
end
236+
237+
return m
238+
end
239+
240+
@generated function _norm_scaled(::Size{S}, a::StaticArray) where {S}
241+
expr = :(LinearAlgebra.norm_sqr(a[1]/scale))
242+
for j = 2:prod(S)
243+
expr = :($expr + LinearAlgebra.norm_sqr(a[$j]/scale))
244+
end
245+
246+
return quote
247+
$(Expr(:meta, :inline))
248+
scale = maxabs_nested(a)
249+
250+
scale==0 && return _init_zero(a)
251+
return @inbounds scale * sqrt($expr)
252+
end
253+
end
254+
228255
@inline norm(a::StaticArray) = _norm(Size(a), a)
229256
@generated function _norm(::Size{S}, a::StaticArray) where {S}
230-
if prod(S) == 0
231-
return :(_init_zero(a))
232-
end
257+
prod(S) == 0 && return :(_init_zero(a))
233258

234259
expr = :(LinearAlgebra.norm_sqr(a[1]))
235260
for j = 2:prod(S)
@@ -238,7 +263,10 @@ end
238263

239264
return quote
240265
$(Expr(:meta, :inline))
241-
@inbounds return sqrt($expr)
266+
l = @inbounds sqrt($expr)
267+
268+
0<l<Inf && return l
269+
return _norm_scaled(Size(a), a)
242270
end
243271
end
244272

@@ -247,11 +275,31 @@ function _norm_p0(x)
247275
return float(norm(iszero(x) ? zero(T) : one(T)))
248276
end
249277

278+
# Do not need to deal with p == 0, 2, Inf; see norm(a, p).
279+
@generated function _norm_scaled(::Size{S}, a::StaticArray, p::Real) where {S}
280+
expr = :(norm(a[1]/scale)^p)
281+
for j = 2:prod(S)
282+
expr = :($expr + norm(a[$j]/scale)^p)
283+
end
284+
285+
expr_p1 = :(norm(a[1]/scale))
286+
for j = 2:prod(S)
287+
expr_p1 = :($expr_p1 + norm(a[$j]/scale))
288+
end
289+
290+
return quote
291+
$(Expr(:meta, :inline))
292+
scale = maxabs_nested(a)
293+
294+
scale==0 && return _init_zero(a)
295+
p == 1 && return @inbounds scale * $expr_p1
296+
return @inbounds scale * ($expr)^(inv(p))
297+
end
298+
end
299+
250300
@inline norm(a::StaticArray, p::Real) = _norm(Size(a), a, p)
251301
@generated function _norm(::Size{S}, a::StaticArray, p::Real) where {S}
252-
if prod(S) == 0
253-
return :(_init_zero(a))
254-
end
302+
prod(S) == 0 && return :(_init_zero(a))
255303

256304
expr = :(norm(a[1])^p)
257305
for j = 2:prod(S)
@@ -265,17 +313,13 @@ end
265313

266314
return quote
267315
$(Expr(:meta, :inline))
268-
if p == Inf
269-
return mapreduce(norm, max, a)
270-
elseif p == 1
271-
@inbounds return $expr_p1
272-
elseif p == 2
273-
return norm(a)
274-
elseif p == 0
275-
return mapreduce(_norm_p0, +, a)
276-
else
277-
@inbounds return ($expr)^(inv(p))
278-
end
316+
p == 0 && return mapreduce(_norm_p0, +, a) # no need for scaling
317+
p == 2 && return norm(a) # norm(a) takes care of scaling
318+
p == Inf && return mapreduce(norm, max, a) # no need for scaling
319+
320+
l = p==1 ? @inbounds($expr_p1) : @inbounds(($expr)^(inv(p)))
321+
0<l<Inf && return l
322+
return _norm_scaled(Size(a), a, p) # p != 0, 2, Inf
279323
end
280324
end
281325

@@ -468,4 +512,3 @@ end
468512
# Some shimming for special linear algebra matrix types
469513
@inline LinearAlgebra.Symmetric(A::StaticMatrix, uplo::Char='U') = (checksquare(A); Symmetric{eltype(A),typeof(A)}(A, uplo))
470514
@inline LinearAlgebra.Hermitian(A::StaticMatrix, uplo::Char='U') = (checksquare(A); Hermitian{eltype(A),typeof(A)}(A, uplo))
471-

test/linalg.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ StaticArrays.similar_type(::Union{RotMat2,Type{RotMat2}}) = SMatrix{2,2,Float64,
236236
end
237237

238238
@testset "normalization" begin
239+
@test norm(SVector(0.0,1e-180)) == 1e-180 # avoid underflow
240+
@test norm(SVector(0.0,1e155)) == 1e155 # avoid overflow
241+
@test all([norm(SVector(0.0,1e-180), p) == 1e-180 for p = [2,3,Inf]]) # avoid underflow
242+
@test all([norm(SVector(0.0,1e155), p) == 1e155 for p = [2,3,Inf]]) # avoid overflow
239243
@test norm(SVector(1.0,2.0,2.0)) 3.0
240244
@test norm(SVector(1.0,2.0,2.0),2) 3.0
241245
@test norm(SVector(1.0,2.0,2.0),Inf) 2.0

0 commit comments

Comments
 (0)