[racket-dev] Contract barrier inefficiencies

From: Neil Toronto (neil.toronto at gmail.com)
Date: Fri Dec 28 11:43:22 EST 2012

On 12/28/2012 08:23 AM, Matthias Felleisen wrote:
> On Dec 27, 2012, at 9:22 PM, Neil Toronto wrote:
>> Sorry it took so long to reply.
>>
>> I applied the patch and verified that my example runs a *lot* faster (with domain and range contracts identical, of course). Unfortunately, a similar test using arrays isn't any faster. This is how the identity function for arrays is defined in the test:
>>
>>   (: array-id (All (A) ((Array A) -> (Array A))))
>>   (define (array-id arr) arr)
>
> If we had macros for types, we might be able to write
>
>   (All (A) (let ((t (Array A))) (t -> t)))
>
>> The change doesn't help because the contracts for each instance of (Array A) in the type aren't identical. Merging such contracts would be a great space optimization in general, but I don't know whether it would make using `math/array' from untyped code any faster.
>>
>> Actually, right now, I'm sure it wouldn't help much. I can't think of any array functions that return an array with an identical procedure
>
> It is not an identical procedure that makes for contract optimizations here but identical contracts/types. Would that help?

Hmm... it might, if the merging were thorough enough (i.e. merging 
contracts from different modules) or `contract-stronger?' were used. By 
the time an array is operated on in Typed Racket code, I think it's had 
its procedure wrapped with two equivalent higher-order contracts. An 
example:

#lang racket
(require math/array)

(define arr (make-array #(100 100) 0))
(array->mutable-array arr)

So `arr' has an (Array any/c) contract from being returned from 
`make-array' across the barrier. When `array->mutable-array' operates on 
it, it crosses the barrier again, so an (Array any/c) contract is applied.

What would be really nice is if passing the array back in to typed code 
*removed* the contract put on it coming out, or removed parts of it. For 
the array's procedure, the typed code needs a ((Vectorof Index) -> A). 
It gets a value with the contract ((vectorof index?) . -> . any/c). The 
typed code has been statically verified to not violate that contract. 
Would it be sound for the barrier to detect that and unwrap the value?

Neil ⊥

>> On 12/18/2012 12:04 PM, Robby Findler wrote:
>>> I can't help with the others, but for 1.) I've considered using a
>>> contract-stronger? test or even just an eq? test when applying a
>>> contract, checking what's already on the value. I haven't added that as
>>> it hasn't come up "in anger" (and it isn't free, but the check is
>>> cheap). If I put a broken test (diff below) and adjust your example a
>>> little bit, it runs quickly for me. Does it also help in your larger
>>> context?
>>>
>>> Adjustment:
>>>
>>> (module defs racket
>>>    (define c (any/c . -> . any/c))
>>>    (provide (contract-out [fun-id  (-> c c)]))
>>>    (define (fun-id f) f))
>>>
>>> Diff:
>>>
>>> [robby at wireless-165-124-98-86] ~/git/plt/collects/racket$ git diff . | cat
>>> diff --git a/collects/racket/contract/private/arrow.rkt
>>> b/collects/racket/contract/private/arrow.rkt
>>> index 55ed632..63c9a6e 100644
>>> --- a/collects/racket/contract/private/arrow.rkt
>>> +++ b/collects/racket/contract/private/arrow.rkt
>>> @@ -506,6 +506,10 @@ v4 todo:
>>>                                  partial-mandatory-kwds
>>> partial-optional-kwds
>>>                                  partial-ranges))
>>>         (λ (val)
>>> +        (cond
>>> +          [(eq? (value-contract val) ctc)
>>> +           val]
>>> +          [else
>>>           (if has-rest?
>>>               (check-procedure/more val mtd? dom-length
>>> mandatory-keywords optional-keywords orig-blame)
>>>               (check-procedure val mtd? dom-length optionals-length
>>> mandatory-keywords optional-keywords orig-blame))
>>> @@ -521,7 +525,7 @@ v4 todo:
>>>                impersonator-prop:contracted ctc
>>>                impersonator-prop:application-mark (cons contract-key
>>>                                                         ;; is this right?
>>> -                                                      partial-ranges)))))))
>>> +
>>>   partial-ranges)))])))))
>>>   (define (->-name ctc)
>>>     (single-arrow-name-maker
>>>
>>>
>>> The diff is broken because it doesn't account for blame. The test should
>>> be making sure that the two contracts blame the same things
>>> (alternatively, we could not check that and just drop the inner
>>> contract. That may be better, but not as easy to try ...)
>>>
>>> Robby
>>>
>>>
>>> On Tue, Dec 18, 2012 at 12:06 PM, Neil Toronto <neil.toronto at gmail.com
>>> <mailto:neil.toronto at gmail.com>> wrote:
>>>
>>>     So there are potentially huge inefficiencies when mixing typed and
>>>     untyped code, usually when functions are passed across the contract
>>>     barrier. Here are a few things that I think cause them. Can the
>>>     People Who Know This Stuff Really Well please comment on these issues?
>>>
>>>     ====
>>>     1. Functions that cross the contract barrier can have exactly the
>>>     same contract applied multiple times. Here's some benchmark code
>>>     that demonstrates the issue:
>>>
>>>     #lang racket
>>>
>>>     (module defs racket
>>>        (provide (contract-out [fun-id  (-> (any/c . -> . any/c)
>>>                                            (any/c . -> . any/c))]))
>>>        (define (fun-id f) f))
>>>
>>>     (require 'defs)
>>>
>>>     (define (f x) x)
>>>
>>>     (define h
>>>        (for/fold ([h f]) ([_  (in-range 1000)])
>>>          (fun-id h)))
>>>
>>>     (for ([_  (in-range 5)])
>>>        (time (for ([_  (in-range 1000)])
>>>                (h 5))))
>>>
>>>
>>>     I get over 800ms for each 1000 applications of `h', because it's
>>>     basically a 1000-deep wrapper. (Or is it 2000?)
>>>
>>>     (The contract system is smart enough to check the contract quickly
>>>     when the types are (any/c . -> . any), but I don't think TR ever
>>>     generates contracts using `any'.)
>>>
>>>     This is a problem for `math/array' because array procedures are
>>>     wrapped going in and going out with exactly the same contract:
>>>     ((vectorof index?) . -> . any/c).
>>>
>>>     ====
>>>     2. It seems TR checks more things than it really has to. In this
>>>     example, the predicate `foo?' prints something so we can observe
>>>     when it's applied, and is used as a predicate for an opaque type
>>>     whose values cross the contract barrier:
>>>
>>>     #lang racket
>>>
>>>     ;; Provides a predicate and constructor for the opaque type `Foo'
>>>     (module foo-defs racket
>>>        (provide foo? make-foo)
>>>
>>>        (define (make-foo x) x)
>>>
>>>        (define (foo? x)
>>>          (printf "foo?~n")
>>>          (exact-integer? x))
>>>        )
>>>
>>>     (module typed-defs typed/racket
>>>        (provide get-foo foo-ap)
>>>
>>>        (require/typed
>>>         (submod ".." foo-defs)
>>>         [#:opaque Foo foo?]
>>>         [make-foo (Integer -> Foo)])
>>>
>>>        ;; prints `foo?' 10 times; definitely necessary
>>>        (define foos (build-vector 10 make-foo))
>>>
>>>        (: get-foo (Integer -> Foo))
>>>        (define (get-foo i)
>>>          (vector-ref foos i))
>>>
>>>        (: foo-ap (All (A) ((Foo -> A) Foo -> A)))
>>>        (define (foo-ap f foo)
>>>          (f foo))
>>>        )
>>>
>>>     (require 'typed-defs)
>>>
>>>
>>>     I don't understand why the contract for `get-foo' has to check the
>>>     return value, because TR already ensures that `get-foo' always
>>>     returns a `Foo':
>>>
>>>        (printf "going to get a foo~n")
>>>        (get-foo 5)  ; prints `foo?' once; why?
>>>
>>>     Could TR generate (exact-integer? . -> . any) for `get-foo'?
>>>
>>>     Relatedly, TR already ensures that the function passed to `foo-ap'
>>>     is only applied to `Foo' values, but this is also checked by a contract:
>>>
>>>        (printf "going to apply a function to a foo~n")
>>>        (foo-ap identity 1)  ; prints `foo?' twice; why not once, just for 1?
>>>
>>>     ====
>>>     3. It's a shame we can't know statically whether an untyped function
>>>     will return more than one value.
>>>
>>>     Suppose we could, and that TR generated contracts with `any/c'
>>>     argument types for higher-order functions (i.e. fix issue #2). Then
>>>     array procedures passed from untyped to typed code could have the
>>>     contract (any/c . -> . any), which is checked immediately.
>>>
>>>     There's probably more, but this will do for now.
>>>
>>>     Neil ⊥
>>>     _________________________
>>>       Racket Developers list:
>>>     http://lists.racket-lang.org/__dev <http://lists.racket-lang.org/dev>
>>>
>>>
>>
>> _________________________
>> Racket Developers list:
>> http://lists.racket-lang.org/dev
>


Posted on the dev mailing list.