jeudi 7 avril 2016

Is it possible to organize sanity checks in Julia to make them easy to compile out when loading a package?

I am developing a package that needs to run fast and be correct. I want to write just one function, but have two "versions" of this function: one that immediately stops when it detects any sort of funny business whatsoever, and another that just runs as fast as possible. My idea is to run the strict version of the function on a random sample of inputs, and provided that nothing fails, run the fast version on the entire set of inputs. I definitely don't have the computing power to run every check on every input, even though I would sleep easier if I could. (I'm open to the idea that this is already the wrong way to approach the verification problem, so if you have a better organizing approach feel free to suggest that instead of a specific improvement to below.)

So far, I've hacked up a temporary solution to this problem as follows:

module Foo

export some_function

const using_removeable_assertions = true

macro RemoveablyAssert(the_condition)
    if using_removeable_assertions
        return esc(:($the_condition || error("Assertion failed.")))
    else
        return esc("")
    end
end

function some_function(x::Float64,y::Float64)
    #Say this should only be called on positive numbers
    @RemoveablyAssert x>0
    @RemoveablyAssert y>0
    (x + y/2.0)::Float64
end

end

using Foo

So this works,

julia> some_function(1.0,1.0)
1.5

julia> some_function(1.0,-1.0)
ERROR: Assertion failed.
 in some_function at none:18

And if I reload the module where I've manually changed const using_removeable_assertions = false, then not only does it skip those checks,

julia> some_function(1.0,1.0)
1.5

julia> some_function(1.0,-1.0)
0.5

but in fact, as intended, the native code is identical to the same function where I never even put those lines:

julia> @code_native some_function(1.0,1.0)
    .section    __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 19
    pushq   %rbp
    movq    %rsp, %rbp
    movabsq $13174486592, %rax      ## imm = 0x31142B640
Source line: 19
    vmulsd  (%rax), %xmm1, %xmm1
    vaddsd  %xmm0, %xmm1, %xmm0
    popq    %rbp
    ret

E.g. if I define

function some_function_2(x::Float64,y::Float64)
    (x + y/2.0)::Float64
end

Then

julia> @code_native some_function_2(1.0,1.0)
    .section    __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 2
    pushq   %rbp
    movq    %rsp, %rbp
    movabsq $13174493536, %rax      ## imm = 0x31142D160
Source line: 2
    vmulsd  (%rax), %xmm1, %xmm1
    vaddsd  %xmm0, %xmm1, %xmm0
    popq    %rbp
    ret

So: these are two of the desired properties of a solution,

  1. Turning on / off a big set of strict sanity checks should be about as simple as flipping a switch
  2. In the "off" case, the native code should be completely clean of any references to the checks; in particular, there should be no performance difference in the off case

but I would like a solution that is a little less manual and a little more idiomatic or standard.

Note, it's impractical to manually define two versions of every function because maintaining them would be a mess. So I've thought about using another macro that generates a some_function_safe for each some_function and then just manually running the safe checks that way, but I'm hoping there's a better solution, something akin to a command line argument to julia, but specific to the module. E.g. using[safemode=yes] Foo.

Relatedly, I wonder if there's a good approach to handling multiple check "levels," beyond some similar hack e.g. defining a global "safety level" and then using macros to conditionally inject checks provided the global safety level exceeds the specific check level. If the [safemode=yes] idea above could be implemented, then certainly this could be implemented as well, but I'm wondering whether there's just a better way to organize these sorts of checks.

(FWIW, I consider these checks complements to, not substitutes for, a big test suite. The test suite alone doesn't make me sleep easy. Also, though my example is of argument verification, I want to use these checks not only for pre- and post-conditions but also for all sorts of sanity checks, e.g. checking invariants in bodies of methods and writing in specific checks against mistakes I've already made and fixed, etc.)

Aucun commentaire:

Enregistrer un commentaire