Skip to content

Add checked, wrapping and saturating arithmetic for add/sub/neg #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2020

Conversation

kimikage
Copy link
Collaborator

@kimikage kimikage commented Jul 7, 2020

Closes #152.

The default arithmetic is still the wrapping arithmetic.

julia> using CheckedArithmetic

julia> @checked N0f8(0) + N0f8(1)
1.0N0f8

julia> @checked N0f8(0.5) + N0f8(0.5)
ERROR: OverflowError: 128 + 128 overflowed for type UInt8

julia> @checked 1N0f8 # Maybe some other time.
ERROR: MethodError: no method matching checked_mul(::Int64, ::Type{Normed{UInt8,8}})

julia> wrapping_add(0.5N0f8, 0.5N0f8)
0.0N0f8

julia> saturating_add(0.5N0f8, 0.5N0f8)
1.0N0f8

We may want to customize the overflow message.

@kimikage kimikage marked this pull request as draft July 7, 2020 04:13
@kimikage
Copy link
Collaborator Author

kimikage commented Jul 7, 2020

Although I didn't intend to work on optimization in this PR, I added the specialized methods for unsigned saturation operation.
Even though SSE2 and AVX2 have 8-bit/16-bit saturation add/sub instructions (e.g. vpaddusb), they are not used on Julia v1.4.2.

It may be better for Julia to support the LLVM's saturation arithmetic intrinsics. Perhaps that should have been discussed, but I couldn't find the latest info.

Anyway, {add/sub}_with_overflow are really terrible in vectorization. 😭 I don't like Matlab as a programming language, so I don't care the sighs of Matlab users. However I feel the need for a workaround.

julia> x_n0f8 = collect(rand(N0f8, 1000, 1000)); y_n0f8 = collect(rand(N0f8, 1000, 1000));

julia> x_q0f7 = collect(rand(Q0f7, 1000, 1000)); y_q0f7 = collect(rand(Q0f7, 1000, 1000));

julia> @btime wrapping_add.($x_n0f8, $y_n0f8); # Julia v1.4.2, v1.5.0
  87.900 μs (2 allocations: 976.70 KiB)

julia> @btime saturating_add.($x_n0f8, $y_n0f8); # Julia v1.4.2
  424.299 μs (2 allocations: 976.70 KiB)

julia> @btime saturating_add.($x_n0f8, $y_n0f8); # Julia v1.5.0 (using AVX2 vpaddusb instruction)
  98.499 μs (2 allocations: 976.70 KiB)
julia> @btime saturating_add.($x_q0f7, $y_q0f7); # Julia v1.4.2 using add_with_overflow
  2.523 ms (2 allocations: 976.70 KiB)

julia> @btime saturating_add.($x_q0f7, $y_q0f7); # Julia v1.5.0 using add_with_overflow
  679.901 μs (2 allocations: 976.70 KiB)

Edit:
Since Julia v1.5.0 was released, I tried not to use {add/sub}_with_overflow.

Julia v1.4.2 Julia v1.5.0
wrapping_add.(x_n0f8, y_n0f8) 87.501 μs 87.799 μs
saturating_add.(x_n0f8, y_n0f8) 428.801 μs 98.000 μs
saturating_sub.(x_n0f8, y_n0f8) 350.700 μs 101.801 μs
saturating_add.(x_q0f7, y_q0f7) 642.600 μs 186.901 μs
saturating_sub.(x_q0f7, y_q0f7) 558.400 μs 156.299 μs

They are still slow, but it's better than not doing anything.

@codecov
Copy link

codecov bot commented Jul 7, 2020

Codecov Report

Merging #190 into master will increase coverage by 0.34%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #190      +/-   ##
==========================================
+ Coverage   87.89%   88.24%   +0.34%     
==========================================
  Files           6        6              
  Lines         471      485      +14     
==========================================
+ Hits          414      428      +14     
  Misses         57       57              
Impacted Files Coverage Δ
src/FixedPointNumbers.jl 84.24% <100.00%> (+1.46%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3e41a6a...0042f3f. Read the comment docs.

@kimikage kimikage marked this pull request as ready for review July 18, 2020 22:44
@kimikage kimikage changed the title [RFC] Add checked, wrapping and saturating arithmetic for add/sub/neg Add checked, wrapping and saturating arithmetic for add/sub/neg Jul 24, 2020
@kimikage kimikage force-pushed the checked_add branch 2 times, most recently from 32f7463 to 7248e33 Compare August 8, 2020 04:35
@kimikage kimikage requested a review from timholy August 8, 2020 04:58
Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice bit twiddling!

Do we need to check any intermediate values? All the tests use extreme values (typemin/typemax/zero/eps).

@kimikage
Copy link
Collaborator Author

I foresee that the 100% test of 16-bit types for division family will take time. (Of course, the division operation itself should not be slow.)

However, I feel like we can test the 8-bit types.

@timholy
Copy link
Member

timholy commented Aug 10, 2020

That's kind of what I was thinking too.

@kimikage
Copy link
Collaborator Author

I added the 8-bit :thin tests.
We may need to be serious about commonizing our tests (cf. #139 (comment)). 😅

The default arithmetic is still the wrapping arithmetic.
@kimikage
Copy link
Collaborator Author

There are still opportunities to review the test code. I plan to improve the multiplication implementation in two or three PRs after this.

@kimikage kimikage merged commit b7e2ae1 into JuliaMath:master Aug 13, 2020
@kimikage kimikage deleted the checked_add branch August 13, 2020 17:19
@kimikage
Copy link
Collaborator Author

kimikage commented Aug 15, 2020

Checked arithmetic may throw an exception, so a slowdown is unavoidable. Instead of checked arithmetic, wrapping and saturating arithmetic can be used to provide some mitigation.

julia> x = rand(N0f8, 1000, 1000);

julia> y = typemax(N0f8) .- x;

julia> function checked_add_arrays(x, y)
           s = saturating_add.(x, y)
           all(wrapping_add.(x, y).==s) || throw(OverflowError("overflowed somewhere"))
           s
       end
checked_add_arrays (generic function with 1 method)

julia> @btime wrapping_add.($x, $y);
  86.299 μs (2 allocations: 976.70 KiB)

julia> @btime checked_add.($x, $y);
  898.900 μs (2 allocations: 976.70 KiB) # Windows
  678.800 μs (2 allocations: 976.70 KiB) # Linux

julia> @btime checked_add_arrays($x, $y);
  313.200 μs (6 allocations: 1.08 MiB)

Edit:
Of course, the same technique can be used for RGB types as well.

function checked_add(x::C, y::C) where {C <: AbstractRGB{<:FixedPoint}}
    s = mapc(saturating_add, x, y)
    s === mapc(wrapping_add, x, y) || throw(OverflowError(""))
    s
end

@kimikage kimikage mentioned this pull request Apr 30, 2024
38 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add checked_add and checked_sub
2 participants