* [Caml-list] @, List.append, and tail recursion @ 2003-01-24 0:48 Brian Hurt 2003-01-30 18:10 ` Olivier Andrieu 0 siblings, 1 reply; 16+ messages in thread From: Brian Hurt @ 2003-01-24 0:48 UTC (permalink / raw) To: Ocaml Mailing List I hit a bug recently wiith @ and List.append. Since they're recursive, not tail-recursive, on long enough lists Ocaml thinks you've gone infinitely recursive and aborts. The code: let longlist len = let rec longlist_int v c acc = if (c == 0) then acc else longlist_int (v + 1) (c - 1) (v :: acc) in longlist_int 0 len [] ;; let x = longlist 65536 ;; List.append x [] ;; Exits with: Stack overflow during evaluation (looping recursion?). So does: x @ [] ;; You can work around this like: let append' a b = List.rev_append (List.rev a) b ;; Since both rev_append and rev are tail recursive (looping) and not recursive, this works. But some ad-hoc testing says that this method is about 50% slower than normal append for lists short enough not to abort. Thinking about this, I realized that my code is doing stuff like this all over the place. I'm basically doing sparse vector/matrix stuff, handling (effectively) (colno * value) list for vectors, and (rowno * vector) list for matrix. And I may be hitting lists long enough to trip the problem. Which means I'm currently doing a lot of recursion of the form: let rec foo x = match x with [] -> [] | head :: tail -> (expr head) :: (foo tail) ;; for various complexities. And it has occured to me that all of these forms *should* be optimizable into loops. The general case would work something like this in C: struct list_t { void * datum; struct list_t * next_p; } struct list_t * foo (struct list_t * x) { struct list_t * retval = NULL; struct list_t ** ptr_pp = &retval; while (x != NULL) { struct list_t * temp = alloc(sizeof(struct list_t)); *ptr_pp = temp; temp->datum = expr(x->datum); temp->next_p = NULL; /* be nice to the GC */ ptr_pp = &(temp->next_p); x = x->next_p; } return retval; } If expr() returned a list, the only change necessary would be to find the end of the list before moving on, like: struct list_t * foo (struct list_t * x) { struct list_t * retval = NULL; struct list_t ** ptr_pp = &retval; while (x != NULL) { *ptr_p = expr(x->datum); /* expr allocates the list */ /* We assume the last element of the list expr() returned has * NULL for next_p. */ while (*ptr_p != NULL) { ptr_p = &((*ptr_p)->next_p); } x = x->next_p; } return retval; } Rather than just looking at making @ an inline C function, I think we (the Ocaml community) should be looking at adding this more general optimization in. So now we get to my two questions: a) is anyone working on this/intending to work on this RSN? b) if the answer to (a) is no, can anyone give me some pointers on where to start looking at code, so I can add it in? Brian ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-24 0:48 [Caml-list] @, List.append, and tail recursion Brian Hurt @ 2003-01-30 18:10 ` Olivier Andrieu 2003-01-30 19:46 ` Brian Hurt 0 siblings, 1 reply; 16+ messages in thread From: Olivier Andrieu @ 2003-01-30 18:10 UTC (permalink / raw) To: Brian Hurt; +Cc: Ocaml Mailing List Brian Hurt [Thursday 23 January 2003] : > I hit a bug recently wiith @ and List.append. Since they're recursive, > not tail-recursive, on long enough lists Ocaml thinks you've gone > infinitely recursive and aborts. > List.append x [] ;; > Exits with: > Stack overflow during evaluation (looping recursion?). > > So does: > x @ [] ;; (not surprising, List.append 's definition is (@)) > And it has occured to me that all of these forms *should* be > optimizable into loops. The general case would work something like > this in C: > > struct list_t { > void * datum; > struct list_t * next_p; > } > > struct list_t * foo (struct list_t * x) { > struct list_t * retval = NULL; > struct list_t ** ptr_pp = &retval; > > while (x != NULL) { > struct list_t * temp = alloc(sizeof(struct list_t)); > *ptr_pp = temp; > temp->datum = expr(x->datum); > temp->next_p = NULL; /* be nice to the GC */ > ptr_pp = &(temp->next_p); > x = x->next_p; > } > return retval; > } Well, all of this can be translated directly to caml, using the famous Obj module. All you need is a lispish setcdr function : ,---- | let setcdr : 'a list -> 'a list -> unit = fun c v -> | let c = Obj.repr c in | assert(Obj.is_block c) ; | Obj.set_field c 1 (Obj.repr v) `---- Then one can write a tail-recursive append or a tail-recursive map : ,---- | let tr_append l1 l2 = | let rec proc cell = function | | [] -> | setcdr cell l2 | | x :: l -> | let nxt = [ x ] in | setcdr cell nxt ; | proc nxt l | in | match l1 with | | [] -> l2 | | x :: l -> | let head = [ x ] in | proc head l ; | head | | let tr_map f l = | let rec proc cell = function | | [] -> () | | x :: l -> | let nxt = [ f x ] in | setcdr cell nxt ; | proc nxt l | in | match l with | | [] -> [] | | x :: l -> | let head = [ f x ] in | proc head l ; | head `---- This seems safe to me, as setcdr is only called on new cons cells, so it shouldn't mess up the arguments. Anyway, the hole abstraction thing is cleaner but needs compiler support. -- Olivier ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-30 18:10 ` Olivier Andrieu @ 2003-01-30 19:46 ` Brian Hurt 2003-01-30 20:52 ` Olivier Andrieu 0 siblings, 1 reply; 16+ messages in thread From: Brian Hurt @ 2003-01-30 19:46 UTC (permalink / raw) To: Olivier Andrieu; +Cc: Ocaml Mailing List [-- Attachment #1: Type: TEXT/PLAIN, Size: 3095 bytes --] For short lists, this is the worst performer overall. I whipped up a quick microbenchmark to compare the various implementations- the three implementations are included in this email. The three programs are: list1.ml: lappend uses the @ operator to append the list list2.ml: uses local rev_append and rev functions (similiar to those in List) to append the list list3.ml: uses Olivier's set_cdr function. The results I saw (compiling with ocamlopt -o list list.ml on a 1.4GHz P4 running Linux and ocaml 3.06) are: list1: 1.462s list2: 1.757s list3: 1.824s List2 is about 17% slower than list1, while list3 is almost 20% slower than list1, and 4% slower than list2. Brian On Thu, 30 Jan 2003, Olivier Andrieu wrote: > Brian Hurt [Thursday 23 January 2003] : > > I hit a bug recently wiith @ and List.append. Since they're recursive, > > not tail-recursive, on long enough lists Ocaml thinks you've gone > > infinitely recursive and aborts. > > > List.append x [] ;; > > Exits with: > > Stack overflow during evaluation (looping recursion?). > > > > So does: > > x @ [] ;; > (not surprising, List.append 's definition is (@)) > > > And it has occured to me that all of these forms *should* be > > optimizable into loops. The general case would work something like > > this in C: > > > > struct list_t { > > void * datum; > > struct list_t * next_p; > > } > > > > struct list_t * foo (struct list_t * x) { > > struct list_t * retval = NULL; > > struct list_t ** ptr_pp = &retval; > > > > while (x != NULL) { > > struct list_t * temp = alloc(sizeof(struct list_t)); > > *ptr_pp = temp; > > temp->datum = expr(x->datum); > > temp->next_p = NULL; /* be nice to the GC */ > > ptr_pp = &(temp->next_p); > > x = x->next_p; > > } > > return retval; > > } > > Well, all of this can be translated directly to caml, using the famous > Obj module. All you need is a lispish setcdr function : > > ,---- > | let setcdr : 'a list -> 'a list -> unit = fun c v -> > | let c = Obj.repr c in > | assert(Obj.is_block c) ; > | Obj.set_field c 1 (Obj.repr v) > `---- > > Then one can write a tail-recursive append or a tail-recursive map : > ,---- > | let tr_append l1 l2 = > | let rec proc cell = function > | | [] -> > | setcdr cell l2 > | | x :: l -> > | let nxt = [ x ] in > | setcdr cell nxt ; > | proc nxt l > | in > | match l1 with > | | [] -> l2 > | | x :: l -> > | let head = [ x ] in > | proc head l ; > | head > | > | let tr_map f l = > | let rec proc cell = function > | | [] -> () > | | x :: l -> > | let nxt = [ f x ] in > | setcdr cell nxt ; > | proc nxt l > | in > | match l with > | | [] -> [] > | | x :: l -> > | let head = [ f x ] in > | proc head l ; > | head > `---- > This seems safe to me, as setcdr is only called on new cons cells, so > it shouldn't mess up the arguments. > > Anyway, the hole abstraction thing is cleaner but needs compiler > support. > > [-- Attachment #2: uses @ operator --] [-- Type: TEXT/PLAIN, Size: 271 bytes --] let lappend x y = x @ [ y ] ;; let makelist c = let rec makelist_int c accum = if (c > 0) then makelist_int (c - 1) (lappend accum c) else (lappend accum c) in makelist_int c [] ;; let _ = makelist 5000;; [-- Attachment #3: uses List.rev --] [-- Type: TEXT/PLAIN, Size: 581 bytes --] let rec rev_append x y = match x with [] -> y | h :: t -> rev_append t (h :: y) ;; let rev x = let rec rev_int x accum = match x with [] -> accum | h :: t -> rev_int t (h :: accum) in rev_int x [] ;; let lappend x y = rev_append (rev x) ( [ y ] ) ;; let makelist c = let rec makelist_int c accum = if (c > 0) then makelist_int (c - 1) (lappend accum c) else (lappend accum c) in makelist_int c [] ;; let _ = makelist 5000;; [-- Attachment #4: uses Olivier's set_cdr --] [-- Type: TEXT/PLAIN, Size: 778 bytes --] let setcdr : 'a list -> 'a list -> unit = fun c v -> let c = Obj.repr c in assert(Obj.is_block c) ; Obj.set_field c 1 (Obj.repr v) ;; let lappend l1 l2 = let rec proc cell = function | [] -> setcdr cell l2 | x :: l -> let nxt = [ x ] in setcdr cell nxt ; proc nxt l in match l1 with | [] -> l2 | x :: l -> let head = [ x ] in proc head l ; head ;; let makelist c = let rec makelist_int c accum = if (c > 0) then makelist_int (c - 1) (lappend accum [ c ]) else (lappend accum [ c ]) in makelist_int c [] ;; let _ = makelist 5000;; ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-30 19:46 ` Brian Hurt @ 2003-01-30 20:52 ` Olivier Andrieu 2003-01-30 21:57 ` Brian Hurt 0 siblings, 1 reply; 16+ messages in thread From: Olivier Andrieu @ 2003-01-30 20:52 UTC (permalink / raw) To: Brian Hurt; +Cc: Ocaml Mailing List Brian Hurt [Thursday 30 January 2003] : > For short lists, this is the worst performer overall. I whipped up a > quick microbenchmark to compare the various implementations- the three > implementations are included in this email. The three programs are: > > list1.ml: lappend uses the @ operator to append the list > > list2.ml: uses local rev_append and rev functions (similiar to those in > List) to append the list > > list3.ml: uses Olivier's set_cdr function. > > The results I saw (compiling with ocamlopt -o list list.ml on a 1.4GHz P4 > running Linux and ocaml 3.06) are: > > list1: 1.462s > list2: 1.757s > list3: 1.824s There's an assert in setcdr : it's important because the first argument mustn't be an empty list. It's never the case here, so you can safely compile with -noassert. On my hardware list3 seems to be a teeny bit faster than list1 but anyway, since list2 is just barely slower, I'm not sure it's worth the trouble. -- Olivier ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-30 20:52 ` Olivier Andrieu @ 2003-01-30 21:57 ` Brian Hurt 2003-01-31 2:16 ` james woodyatt 0 siblings, 1 reply; 16+ messages in thread From: Brian Hurt @ 2003-01-30 21:57 UTC (permalink / raw) To: Olivier Andrieu; +Cc: Ocaml Mailing List On Thu, 30 Jan 2003, Olivier Andrieu wrote: > > list1: 1.462s > > list2: 1.757s > > list3: 1.824s > > There's an assert in setcdr : it's important because the first > argument mustn't be an empty list. It's never the case here, so you > can safely compile with -noassert. Doh! OK- now, compiling with -noassert drops the time to 1.457 seconds (same machine, same environment)- to slightly better than the recursive version. And for the record, I just tested with appending to a list of 500,000 elements, and it worked OK. > > On my hardware list3 seems to be a teeny bit faster than list1 but > anyway, since list2 is just barely slower, I'm not sure it's worth the > trouble. > Correctness rates higher in my book than performance. I think it's bad that @/List.append die due to stack overflow if the lists are too long. I'd perfer the reversing solution- which works correctly so long as there is enough memory- over the recursive solution for that reason alone. Your code is even better. It gives the performance of the recursive version and the correctness of the reversing code- better yet, it doesn't allocate two whole copies of the array, allowing the code to work correctly in even more cases (when there is enough memory for one copy of the list but not two). Brian ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-30 21:57 ` Brian Hurt @ 2003-01-31 2:16 ` james woodyatt 2003-01-31 17:05 ` Diego Olivier Fernandez Pons 2003-01-31 17:13 ` Brian Hurt 0 siblings, 2 replies; 16+ messages in thread From: james woodyatt @ 2003-01-31 2:16 UTC (permalink / raw) To: The Trade; +Cc: Brian Hurt everyone-- Earlier in this thread, I suggested using a queue if you are spending too much time in List.append. Lack of optimized tail recursion is not really a factor compared to the waste of cycles involved in constructing a whole new list just to append a single element on the end. Apparently, nobody seemed to notice what I was talking about. So I'm going to try to make my point again. Sorry if you got it the first time. On Thursday, Jan 30, 2003, at 13:57 US/Pacific, Brian Hurt wrote: > On Thu, 30 Jan 2003, Olivier Andrieu wrote: > >>> list1: 1.462s >>> list2: 1.757s >>> list3: 1.824s >> >> There's an assert in setcdr : it's important because the first >> argument mustn't be an empty list. It's never the case here, so you >> can safely compile with -noassert. > > Doh! OK- now, compiling with -noassert drops the time to 1.457 seconds > (same machine, same environment)- to slightly better than the recursive > version. For grins, I wrote an equivalent test program. It uses a functional deque instead of a list. (I have written one. It's a component of my Cf library, which remains unreleased at the moment. Markus Mottl has translated several of Chris Okasaki's functional queue implementations into Ocaml, and you can find them on the Hump.) To get started, I timed the 'benchmarks' by running them on my iBook (the 700 MHz G3 model) so I could get a baseline. My little iBook is nowhere near as fast as your cool 1.6GHz P4, but I'm not complaining. The results of my tests were: $ time ./list1 3.690u 0.070s 0:04.14 90.8% 0+0k 0+0io 0pf+0w $ time ./list2 4.180u 0.020s 0:05.01 83.8% 0+0k 0+0io 0pf+0w $ time ./list3 3.700u 0.000s 0:04.49 82.4% 0+0k 0+0io 0pf+0w Not real fast, but fast enough that I don't mind waiting for results. So, what difference does my functional deque implementation make? Glad you asked. My Cf_deque module matches the following signature: (* begin cf_deque.mli *) type 'a t val nil: 'a t module type Direction_T = sig val pop: 'a t -> ('a * 'a t) option val push: 'a -> 'a t -> 'a t end module A: Direction_T module B: Direction_T val cat: 'a t -> 'a t -> 'a t (* end file *) (Actually, this isn't the complete interface. I've written a variety of ancillary functions that make it convenient to work with the objects in a deque without having to translate them into lists, e.g. fold, iterate, etc.) All the functions above are purely functional, and they perform with O(1) average complexity. (Or at least, that's my untrained analysis. I'd like to provide proof of that assertion, and I'm working on getting some help in that effort-- but, I'll have more news on that when I have it.) Here's a variant of the list1.ml test above, which uses my Cf_deque module instead: (* begin t-opt.deque.ml *) open Cf_deque let rec makelist_aux c accum = let accum = B.push c accum in if c > 0 then makelist_aux (pred c) accum else accum let makelist c = makelist_aux c nil ;; let _ = makelist 5000;; (* end file *) Here are the timing results on that same iBook: $ time ./t-opt.deque 0.010u 0.010s 0:00.02 100.0% 0+0k 0+0io 0pf+0w In other words, it's done before enough time passes even to measure it properly. > And for the record, I just tested with appending to a list of 500,000 > elements, and it worked OK. Here is the result of running my version of the test with 500,000 elements: $ time ./t-opt.deque 0.450u 0.080s 0:00.75 70.6% 0+0k 0+0io 0pf+0w It took under a second of wall-clock time. On the other hand, when I modified list3.ml for 500,000 elements, it took *forever* in wall-clock time. I gave up after almost an hour and a half. Ultimately, I killed it with SIGINT before it finished. I have no idea how far it got. Clearly, I need to push a lot more elements into my deque before I will get a timing result that I can measure in heartbeats. Here is the result for 5,000,000 elements: $ time ./t-opt.deque 5.160u 0.510s 0:06.69 84.7% 0+0k 0+0io 0pf+0w At this stage, I noticed my little iBook was just starting to thrash when the program finished. So, I bumped it up to 50,000,000 elements, because I like punishing my computer from time to time-- just to teach it respect. At around 15 seconds into the run, the program turned into the psychedelic pizza delivery service: the pager went into fear and loathing mode, and the pretty Aqua GUI started acting like it was sniffing glue. If I had let it run to completion, it probably would have wedged the machine. (Mac OS X is pretty stable, but it hates resource bombs as much as any other operating system.) None of the listN.ml files were able to bring down the machine like that, no matter how long I let them run-- which should make sense, right? The APPEND function is O(N) for lists. Once N gets beyond a few hundred, the program spends almost all its cycles just copying list cells inside its innermost loop, only occasionally reaching the end of a list and tacking a new cell onto the end before starting over again. The problem is not the language, or the compiler. The problem is the design of the program. The moral of this story: you really should consider using a queue if you find your programs are spending a lot of cycles appending things to very long lists. -- j h woodyatt <jhw@wetware.com> that's my village calling... no doubt, they want their idiot back. ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-31 2:16 ` james woodyatt @ 2003-01-31 17:05 ` Diego Olivier Fernandez Pons 2003-01-31 19:52 ` Brian Hurt 2003-01-31 21:34 ` [Caml-list] @, List.append, and tail recursion Issac Trotts 2003-01-31 17:13 ` Brian Hurt 1 sibling, 2 replies; 16+ messages in thread From: Diego Olivier Fernandez Pons @ 2003-01-31 17:05 UTC (permalink / raw) To: caml-list Bonjour, Some comments on various contributions to this thread Brian Hurt wrote : > I'm basically doing sparse vector/matrix stuff, handling > (effectively) (colno * value) list for vectors, and (rowno * vector) > list for matrix. And I may be hitting lists long enough to trip the > problem. If you are doing sparse matrix operations and you still hit lists long enough to cause a stack overflow, then your matrix must be really huge. If the ordrer of the terms does not matter (or if you can manage with the position information you keep in your sparse matrix) then you just need to write tail recursive functions, not taking care of the list being reversed let rec rev_map f result = function | [] -> result | head :: tail -> rev_map (f head) tail An other solution is to use a suitable data structure for your application : join-lists, catenable lists, double-linked lists ... There are not many applications in which you just can't work around in a simple way... In fact, the only one I know is Grobner bases computation, because : - the ordering on monomials really matters - you have to handle a huge amount of information, even for small systems In this case, the only solution is to write you program in C/assembler, with your own memory manager Here is a extract of FGb web-site (http://calfor.lip6.fr/~jcf/Software/index.html) Limitations * The size of the matrix in the F4 are limited to 50 000 x 50 000. * The size of the biggest coefficient in the result is limited to 9000 digits. * All the input polynomials must be expanded Most of reasonable computations (even in computer algebra) can be made in a functional language. Many computer algebra systems are written in Lisp, and FOC (Formal + OCaml + Coq) should be soon avaible (spring 2003) That is why I suspect you may not be using the best data structures and algorithms avaiable. Could you explain what you are working on ? Christophe Raffalli wrote : > May be rev_append could be added to the list module (despite the > fact it is trivial to write it yoursleft) ? ??? Objective Caml version 3.06 # List.rev_append;; - : 'a list -> 'a list -> 'a list = <fun> > I agree that this is recurring problem, I myself often get bit by > List.map > > It makes it very easy to make non-scalable program, works for input > less that 1000 elements, and the when applied to a large problem it > fails without a trace. It is very difficult to find the location of > the problem if you use the native compiler, and most of these > programs doesn't even work using the byte-code compiler. > > So one of my coding guidelines is: > - do not use List.map > > I would like a prefer other solutions If your program does not work for input less than 1000, this probably means a poor design. You may be using lists where other data structure should be used. Could you give me code examples ? I could then add an adequate data structure to Baire. James woodyatt wrote : > For grins, I wrote an equivalent test program. It uses a functional > deque instead of a list. (I have written one. It's a component of my > Cf library, which remains unreleased at the moment. Markus Mottl has > translated several of Chris Okasaki's functional queue > implementations into Ocaml, and you can find them on the Hump.) You will find in Chris Okasaki's thesis/book several implementations of catenable lists and many references. One of them embedds a queue in a tree which seems to be what you are looking for (head in O(1), append in O(1)) I wrote a linearisation of Okasaki's data structure. It has not been revised yet then there could be some bugs. Diego Olivier ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-31 17:05 ` Diego Olivier Fernandez Pons @ 2003-01-31 19:52 ` Brian Hurt 2003-02-01 10:18 ` Linear systems (was Re: [Caml-list] @, List.append, and tail recursion) Diego Olivier Fernandez Pons 2003-01-31 21:34 ` [Caml-list] @, List.append, and tail recursion Issac Trotts 1 sibling, 1 reply; 16+ messages in thread From: Brian Hurt @ 2003-01-31 19:52 UTC (permalink / raw) To: Diego Olivier Fernandez Pons; +Cc: caml-list On Fri, 31 Jan 2003, Diego Olivier Fernandez Pons wrote: > Bonjour, > > Some comments on various contributions to this thread > > Brian Hurt wrote : > > > I'm basically doing sparse vector/matrix stuff, handling > > (effectively) (colno * value) list for vectors, and (rowno * vector) > > list for matrix. And I may be hitting lists long enough to trip the > > problem. > > If you are doing sparse matrix operations and you still hit lists long > enough to cause a stack overflow, then your matrix must be really > huge. It'd be more accurate to say I'm not sure my lists will stay short enough. On average they are short enough and sparse enough to make using lists worthwhile. I expect on average the lists will be about 20 elements or so long. I should be able to stop thinking here- OK, I'm saving more than enough computation time on the average case that being inefficient on the rare case, where the lists has 30K or more elements in it, is OK. But I want the rare case to *work* correctly, even if inefficiently. With the "naive" non-tail-recursive implementation doesn't. Somewhere above 32K elements in the list the recursion trips the stack overflow. Change the problem in some minor way, and suddenly I'm not generating lists with 32K elements in them, maybe just 30K elements in them, and everything works OK. Optimization is the wrong word. As Olivier's set_cdr shows, the cost of recursion isn't signifigant (kudos to the compiler team). It's a correctness issue more than anything: the code *should* work. > > If the ordrer of the terms does not matter (or if you can manage with > the position information you keep in your sparse matrix) then you just > need to write tail recursive functions, not taking care of the list > being reversed I am keeping the items in order. Because if they're in order, I can write add in O(n). If they're not in order, the best (fastest running) way to write add is to first sort both lists O(n log n) and then do the in-order add. > An other solution is to use a suitable data structure for your > application : join-lists, catenable lists, double-linked lists ... Several reasons, actually: 1) I want to stay as strictly functional as possible. I'm comming from an imperitive background, and thus want to limit how much I fall back on old habits. If it can be done functionally (purely applicative), I want to do it that way. 2) The only thing I'm doing with the list that is at all a problem is non-tail-recursion list construction. And this *should* work correctly. So why should I use some other datastructure when the list primitives do everything I need? In fact, linked lists of some sort are exactly what I want- not even arrays. I want to be able to start creating a list of elements without having to know how long it is, and I'm doing a lot of walking the list and basically no accessing random elements. And I'm always walking the list in the same direction. 3) I perfer to solve the general problem than have to resolve the specific problem every time I hit it. This is even worse because the problem is only likely to show up in production. Test cases with small sets of data won't trigger the problem. > > There are not many applications in which you just can't work around in > a simple way... I don't want to have to work around it. > In this case, the only solution is to write you program in > C/assembler, with your own memory manager You should only be programming in assembler if it absolutely cannot be written in C. You should only be programming in C if you're directly banging on hardware. > > That is why I suspect you may not be using the best data structures > and algorithms avaiable. Could you explain what you are working on ? The short form: solving a system of nonlinear equations via Newton-Kantoravich (sp?). Basically, I compute the Jacobian F'(X) (a matrix of the partial differentials) and the residual F(X) (a vector) and solve F'(X) * Y = F(X) to get my new, improved guess X' = X - Y. Lather, rinse, repeat, until F(X) is close enough to 0. This is basically Newton's method extended to deal with multiple equations in multiple variables. In production I wouldn't be surprised to see systems of 30,000+ equations in 30,000+ variables. The Jacobian matrix is going to be very sparse- I expect the average row to have 20 or fewer non-zero elements. Thus the attraction to sparse vectors. I'm going to be solving it via Gaussian elimination (the Jacobian is likely to be malformed in multiple ways, meaning I can't use any iterative method I know of. And yes, I've looked at iterative methods as advanced as GMRES- they don't work). I think that in general I can bound the size of vectors I'm producing. But there are degenerate cases where I could get above 30K non-zero elements in a vector. The problem can't be solved at a higher level. Sparse vectors are the way to go, and I can't gaurentee that I won't get sparse vectors of greater than 30K elements. > You will find in Chris Okasaki's thesis/book several implementations > of catenable lists and many references. One of them embedds a queue in > a tree which seems to be what you are looking for (head in O(1), > append in O(1)) O(1), or O(log n)? Most tree operations are O(log n). This is one of the things I considered, implementing the sparse vectors as trees- maps, effectively. The problem is that most of the time I want to walk in-order through all non-zero elements of the vector. With a tree/map, this is O(n log n). With a list, it's O(n). I don't see what the resistance here is. Were I jumping up and down demanding someone else do the work, I could see the response being "it's not worth it to us". Which is why I'm doing the work myself. There are, from your point of view, two possibilities: 1) I succeed. In which case, a new set of behaviors become efficient. Since you weren't using those behaviors anyways, you don't care- as nothing else changes. Note that I am not proposing to change the semantics of the language. 2) I fail. In which case nothing changes. This includes I come up with something which breaks other stuff, at which point Xavier & co. go "Thanks, but no thanks". Brian ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Linear systems (was Re: [Caml-list] @, List.append, and tail recursion) 2003-01-31 19:52 ` Brian Hurt @ 2003-02-01 10:18 ` Diego Olivier Fernandez Pons 0 siblings, 0 replies; 16+ messages in thread From: Diego Olivier Fernandez Pons @ 2003-02-01 10:18 UTC (permalink / raw) To: Brian Hurt; +Cc: caml-list Bonjour, Sorry for the buggy code, anyway you should be able to fix it. > In production I wouldn't be surprised to see systems of 30,000+ > equations in 30,000+ variables. The Jacobian matrix is going to be > very sparse- I expect the average row to have 20 or fewer non-zero > elements. Thus the attraction to sparse vectors. I'm going to be > solving it via Gaussian elimination (the Jacobian is likely to be > malformed in multiple ways, meaning I can't use any iterative method > I know of. And yes, I've looked at iterative methods as advanced as > GMRES- they don't work). I think that in general I can bound the > size of vectors I'm producing. But there are degenerate cases where > I could get above 30K non-zero elements in a vector. A 30 000 x 30 000 system of equations is not a toy program. Then, do not expect to solve it straightforwardly. You will obviously hit all system limits (stack, cache, ...). You have to understand that if you really want a robuts program in all cases, you will have to work : You will have to design several data structures for every operation you want (to keep both efficiency and generality) and transformation functions. That is the case for example in the Gröbner basis system I pointed out : several monomial orderings with many transformation functions ... You will have to use all Caml properties (functional, imperative ...) You also need to understand roughtly how does the Caml compiler work (boxing/ unboxing, garbage collection, ...) > But I want the rare case to *work* correctly, even if inefficiently. With > the "naive" non-tail-recursive implementation doesn't. Somewhere above > 32K elements in the list the recursion trips the stack overflow. Change > the problem in some minor way, and suddenly I'm not generating lists with > 32K elements in them, maybe just 30K elements in them, and everything > works OK. Your main problem is that sometimes your lists may be too big for the data structure you have chosen. Then, the best thing to do is to have two data structures, one for small (most of all) systems, a second one for huge (but rare) systems. - lists for small sparse vectors - (say) hashtables for huge sparse vectors You just need to write you own hashtable data structure (because you can then profit of Caml's float array unboxing optimisation by separate collision resolution) type size = int type vector = | List of size * (int * float) list | Hashtable of myhashtbl Most of your code will be purely functional and the program will not break on large data. One more point : floating arithmetic may produce incorrect value by error propagation, even for small systems. If you do want you system to work properly for all data (even not well scaled), you will have to use specific algorithms (pivot choice rules, error bounding, ...). Then, list representation may not be apropriate. > O(1), or O(log n)? Most tree operations are O(log n). It is easy to design a data structure with O(log n) acces to any element and O(1) acces to the first one : imagine a list of increasing perfect trees of size 2^k Diego Olivier ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-31 17:05 ` Diego Olivier Fernandez Pons 2003-01-31 19:52 ` Brian Hurt @ 2003-01-31 21:34 ` Issac Trotts 1 sibling, 0 replies; 16+ messages in thread From: Issac Trotts @ 2003-01-31 21:34 UTC (permalink / raw) To: OCaml Mailing List On Fri, Jan 31, 2003 at 06:05:30PM +0100, Diego Olivier Fernandez Pons wrote: > Bonjour, > > Some comments on various contributions to this thread > > Brian Hurt wrote : > > > I'm basically doing sparse vector/matrix stuff, handling > > (effectively) (colno * value) list for vectors, and (rowno * vector) > > list for matrix. And I may be hitting lists long enough to trip the > > problem. > > If you are doing sparse matrix operations and you still hit lists long > enough to cause a stack overflow, then your matrix must be really > huge. > > If the ordrer of the terms does not matter (or if you can manage with > the position information you keep in your sparse matrix) then you just > need to write tail recursive functions, not taking care of the list > being reversed > > let rec rev_map f result = function > | [] -> result > | head :: tail -> rev_map (f head) tail This doesn't work on OCaml 3.06: "This expression has type 'a but is here used with type 'b -> 'a" How about let rec rev_map f result = function | [] -> result | head :: tail -> rev_map f (f head :: result) tail;; # map ((+) 3) [] [1;2;3;4;5];; - : int list = [8; 7; 6; 5; 4] Issac [snip] -- ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-31 2:16 ` james woodyatt 2003-01-31 17:05 ` Diego Olivier Fernandez Pons @ 2003-01-31 17:13 ` Brian Hurt 2003-01-31 17:42 ` brogoff ` (2 more replies) 1 sibling, 3 replies; 16+ messages in thread From: Brian Hurt @ 2003-01-31 17:13 UTC (permalink / raw) To: james woodyatt; +Cc: The Trade, Brian Hurt On Thu, 30 Jan 2003, james woodyatt wrote: > everyone-- > > Earlier in this thread, I suggested using a queue if you are spending > too much time in List.append. Lack of optimized tail recursion is not > really a factor compared to the waste of cycles involved in > constructing a whole new list just to append a single element on the > end. > > Apparently, nobody seemed to notice what I was talking about. So I'm > going to try to make my point again. Sorry if you got it the first > time. I did get it the first time. I'm just using List.append to illustrate a problem I'm having. The problem is *constructing* lists. If you can construct your list backwards, fine- but if you can't, you end up either not being tail recursive (and blowing up for long lists) or allocating the list twice. Here's an example I have run across. I'm working with sparse vectors, and basically storing them as (int * float) lists. Now, let's write the vector add function. The naive implementation would be: let rec add x y = (* return x + y *) match (x, y) with ([], _) -> y | (_, []) -> x | (((xidx, xval) as xhead) :: xtail, ((yidx, yval) as yhead) :: ytail) -> if (xidx == yidx) then (xidx, xval +. yval) :: (add xtail ytail) else if (xidx < yidx) then xhead :: (add xtail y) else yhead :: (add x ytail) ;; It's simple, and obvious in both what it does and how it does it. Except opps, this isn't tail recursive. If your sparse vectors might be 65536 elements long, this will blow up. So we rewrite to be tail recursive: let add x y = (* return x + y *) let add_int x y accum = match (x, y) with ([], _) -> (List.rev_append accum y) | (_, []) -> (List.rev_append accum x) | (((xidx, xval) as xhead) :: xtail, ((yidx, yval) as yhead) :: ytail) -> if (xidx == yidx) then add_int xtail ytail ((xidx, xval +. yval) :: accum) else if (xidx < yidx) then add_int xtail y (xhead :: accum) else add_int x ytail (yhead :: accum) ;; This makes the function truely tail recursive, except now it's allocating most of the returned vector twice (once as accum, and once in List.rev_append) and it's signifigantly uglier IMHO. Rewritting the code to use set_cdr is the best performaner, but the ugliest yet. And the add function is truely simple. It can handle minor increases in ugliness without losing much. But now consider something rather more complicated- say matrix transposition or matrix multiplication with matricies defined as: type vector_t = (int * float) list ;; (* index * value list *) type matrix_t = (int * vector_t) list ;; (* row index * row vector list *) Now minor uglification becomes major uglification. It'd be nicer just to be able to be able to construct lists forwards instead of backwards. List.append is just an obvious example to be talking about, but the problem is signifigantly more general. Brian > > On Thursday, Jan 30, 2003, at 13:57 US/Pacific, Brian Hurt wrote: > > On Thu, 30 Jan 2003, Olivier Andrieu wrote: > > > >>> list1: 1.462s > >>> list2: 1.757s > >>> list3: 1.824s > >> > >> There's an assert in setcdr : it's important because the first > >> argument mustn't be an empty list. It's never the case here, so you > >> can safely compile with -noassert. > > > > Doh! OK- now, compiling with -noassert drops the time to 1.457 seconds > > (same machine, same environment)- to slightly better than the recursive > > version. > > For grins, I wrote an equivalent test program. It uses a functional > deque instead of a list. (I have written one. It's a component of my > Cf library, which remains unreleased at the moment. Markus Mottl has > translated several of Chris Okasaki's functional queue implementations > into Ocaml, and you can find them on the Hump.) > > To get started, I timed the 'benchmarks' by running them on my iBook > (the 700 MHz G3 model) so I could get a baseline. My little iBook is > nowhere near as fast as your cool 1.6GHz P4, but I'm not complaining. > > The results of my tests were: > > $ time ./list1 > 3.690u 0.070s 0:04.14 90.8% 0+0k 0+0io 0pf+0w > > $ time ./list2 > 4.180u 0.020s 0:05.01 83.8% 0+0k 0+0io 0pf+0w > > $ time ./list3 > 3.700u 0.000s 0:04.49 82.4% 0+0k 0+0io 0pf+0w > > Not real fast, but fast enough that I don't mind waiting for results. > So, what difference does my functional deque implementation make? Glad > you asked. > > My Cf_deque module matches the following signature: > > (* begin cf_deque.mli *) > type 'a t > > val nil: 'a t > > module type Direction_T = sig > val pop: 'a t -> ('a * 'a t) option > val push: 'a -> 'a t -> 'a t > end > > module A: Direction_T > module B: Direction_T > > val cat: 'a t -> 'a t -> 'a t > (* end file *) > > (Actually, this isn't the complete interface. I've written a variety > of ancillary functions that make it convenient to work with the objects > in a deque without having to translate them into lists, e.g. fold, > iterate, etc.) > > All the functions above are purely functional, and they perform with > O(1) average complexity. (Or at least, that's my untrained analysis. > I'd like to provide proof of that assertion, and I'm working on getting > some help in that effort-- but, I'll have more news on that when I have > it.) > > Here's a variant of the list1.ml test above, which uses my Cf_deque > module instead: > > (* begin t-opt.deque.ml *) > open Cf_deque > > let rec makelist_aux c accum = > let accum = B.push c accum in > if c > 0 then makelist_aux (pred c) accum else accum > > let makelist c = makelist_aux c nil > > ;; > let _ = makelist 5000;; > (* end file *) > > Here are the timing results on that same iBook: > > $ time ./t-opt.deque > 0.010u 0.010s 0:00.02 100.0% 0+0k 0+0io 0pf+0w > > In other words, it's done before enough time passes even to measure it > properly. > > > And for the record, I just tested with appending to a list of 500,000 > > elements, and it worked OK. > > Here is the result of running my version of the test with 500,000 > elements: > > $ time ./t-opt.deque > 0.450u 0.080s 0:00.75 70.6% 0+0k 0+0io 0pf+0w > > It took under a second of wall-clock time. On the other hand, when I > modified list3.ml for 500,000 elements, it took *forever* in wall-clock > time. I gave up after almost an hour and a half. Ultimately, I killed > it with SIGINT before it finished. I have no idea how far it got. > > Clearly, I need to push a lot more elements into my deque before I will > get a timing result that I can measure in heartbeats. Here is the > result for 5,000,000 elements: > > $ time ./t-opt.deque > 5.160u 0.510s 0:06.69 84.7% 0+0k 0+0io 0pf+0w > > At this stage, I noticed my little iBook was just starting to thrash > when the program finished. So, I bumped it up to 50,000,000 > elements, because I like punishing my computer from time to time-- just > to teach it respect. > > At around 15 seconds into the run, the program turned into the > psychedelic pizza delivery service: the pager went into fear and > loathing mode, and the pretty Aqua GUI started acting like it was > sniffing glue. If I had let it run to completion, it probably would > have wedged the machine. (Mac OS X is pretty stable, but it hates > resource bombs as much as any other operating system.) > > None of the listN.ml files were able to bring down the machine like > that, no matter how long I let them run-- which should make sense, > right? The APPEND function is O(N) for lists. Once N gets beyond a > few hundred, the program spends almost all its cycles just copying list > cells inside its innermost loop, only occasionally reaching the end of > a list and tacking a new cell onto the end before starting over again. > > The problem is not the language, or the compiler. The problem is the > design of the program. The moral of this story: you really should > consider using a queue if you find your programs are spending a lot of > cycles appending things to very long lists. > > > ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-31 17:13 ` Brian Hurt @ 2003-01-31 17:42 ` brogoff 2003-01-31 19:18 ` Russ Ross 2003-01-31 23:12 ` Issac Trotts 2 siblings, 0 replies; 16+ messages in thread From: brogoff @ 2003-01-31 17:42 UTC (permalink / raw) To: Brian Hurt; +Cc: james woodyatt, The Trade On Fri, 31 Jan 2003, Brian Hurt wrote: > On Thu, 30 Jan 2003, james woodyatt wrote: > > Apparently, nobody seemed to notice what I was talking about. So I'm > > going to try to make my point again. Sorry if you got it the first > > time. > > I did get it the first time. I'm just using List.append to illustrate a > problem I'm having. You don't even need to be mapping to a particularly long list, since your mapping function may be consuming stack space too. The stuff using set_cdr was only ugly if you look under the hood. No one complains about other hidden uses of Obj, so that aproach may make sense as an immediate fix. The main point to me is that since there appears to be a reasonable approach to doing all of this stuff in a safe way that extends the power of ML, it may be a good idea to consider removing this limitation in a future (Ca)ML. There are only a few places where the language itself has annoying limitations, and I'm beginning to believe that this is one of them. The implementation provides a workaround as Olivier demonstrated, but the inability to express this in OCaml is a hole that perhaps needs to be filled (pun intended :). -- Brian ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-31 17:13 ` Brian Hurt 2003-01-31 17:42 ` brogoff @ 2003-01-31 19:18 ` Russ Ross 2003-01-31 19:32 ` Alexander V. Voinov 2003-02-01 2:30 ` brogoff 2003-01-31 23:12 ` Issac Trotts 2 siblings, 2 replies; 16+ messages in thread From: Russ Ross @ 2003-01-31 19:18 UTC (permalink / raw) To: caml-list I'd just like to agree with Brian on this. We seem to have a split in this thread and the two branches should be addressed separately: 1. Help with the specific case being mentioned. I think this has been addressed nicely with several helpful suggestions. 2. Discussions about solving this class of problems. Practical workarounds are an essential part of writing software, but eliminating the need for them is where the computer science comes into play. I was reading Paul Graham's "On Lisp" the other day and came across an example that made me cringe. He pointed to a simple Lisp code fragment and then rewrote it for performance, proudly pointing to the result as an example of what efficient Common Lisp looks like and claiming that the result is competitive with C for speed. That's great that the capability is there, but whenever there is a significant difference between the clean, simple way to do something and the efficient way to do it (where the primary different is language or compiler related, not a fundamental algorithm or data structure change) it indicates an opportunity to improve the language or compiler design. If a systematic rewrite can transform inefficient code into efficient code, why doesn't the language and/or compiler just do it for you or prevent the problem in the first place? (the answers "because it is hard to do" and "I don't see you doing it" are both equally valid, but let's consider the question rhetorical for now) In Graham's example (if I recall correctly) the solution involved annotating variable types (Lisp is dynamically typed) and transforming a recursive function to use an accumulator to allow tail recursion. Lisp is full of this pattern and it does make a big different in performance. ML is statically typed so one could argue that it fixes the first problem already (annotating the types effectively removes dynamic typing from the Lisp function) but the second is still there. Tail recursion is a powerful and efficient construct, but it still requires care on the part of the programmer to make sure that the calls are proper tail calls. I think there are many recursive functions which cannot be transformed easily into tail calls, but there is a large class of functions that can be mechanically transformed using techniques discussed here and elsewhere. I would be interested personally if anyone could point to papers or other resources about this topic. Right now I think there is some low-hanging fruit to be plucked--recognizing and transforming a few simple patterns (code that looks like List.map) would make a big difference in a lot of code. Handling more complex cases is undoubtedly harder, but I think the potential benefits are considerable. I appreciate everyone's input on this subject so far. I was attracted to Caml in the first place because it seemed to be one of the best examples of a language where writing code that is clean and natural coincides with writing code that is efficient. I hope that discussions like this can bridge the gap even more. - Russ p.s. I don't mean to sound critical of Paul Graham here--in the context of Common Lisp his examples were very valuable--he just nicely illustrated my point for me so I picked on him. On Fri, Jan 31, 2003 at 11:13:26AM -0600, Brian Hurt wrote: > I did get it the first time. I'm just using List.append to illustrate a > problem I'm having. > > The problem is *constructing* lists. If you can construct your list > backwards, fine- but if you can't, you end up either not being tail > recursive (and blowing up for long lists) or allocating the list twice. <snip> > Now minor uglification becomes major uglification. It'd be nicer just to > be able to be able to construct lists forwards instead of backwards. > > List.append is just an obvious example to be talking about, but the > problem is signifigantly more general. > > Brian ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-31 19:18 ` Russ Ross @ 2003-01-31 19:32 ` Alexander V. Voinov 2003-02-01 2:30 ` brogoff 1 sibling, 0 replies; 16+ messages in thread From: Alexander V. Voinov @ 2003-01-31 19:32 UTC (permalink / raw) To: Russ Ross; +Cc: caml-list Hi All, Russ Ross wrote: >resources about this topic. Right now I think there is some >low-hanging fruit to be plucked--recognizing and transforming a few >simple patterns (code that looks like List.map) would make a big >difference in a lot of code. Handling more complex cases is >undoubtedly harder, but I think the potential benefits are >considerable. > Yes, yes, yes. And this is a matter of adoption as well. Even if the global compiler strategy can't be changed quickly (to accomodate 'futures' or 'holes' or whatever), let's just make the List module completely and absolutely tail recursive, one way or another. So that any newcomer can quickly map his/her favorite interative patterns of construction of homogeneous data structures (lists, arrays) to maps and folds. Alexander ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-31 19:18 ` Russ Ross 2003-01-31 19:32 ` Alexander V. Voinov @ 2003-02-01 2:30 ` brogoff 1 sibling, 0 replies; 16+ messages in thread From: brogoff @ 2003-02-01 2:30 UTC (permalink / raw) To: caml-list On Fri, 31 Jan 2003, Russ Ross wrote: > Tail recursion is a powerful and efficient construct, but it still > requires care on the part of the programmer to make sure that the > calls are proper tail calls. I think there are many recursive > functions which cannot be transformed easily into tail calls, but > there is a large class of functions that can be mechanically > transformed using techniques discussed here and elsewhere. I would > be interested personally if anyone could point to papers or other > resources about this topic. Right now I think there is some > low-hanging fruit to be plucked--recognizing and transforming a few > simple patterns (code that looks like List.map) would make a big > difference in a lot of code. Handling more complex cases is > undoubtedly harder, but I think the potential benefits are > considerable. For the particular case being discussed, there is a bit of theory, and if you read Minamide's paper you'll see that he comments that six functions from the SML Basis can take advantage of the hole abstraction to be written in tail recursive form: append, take, map, mapPartial, filter, and tabulate. For OCaml's list, we I think it's pretty clear that fold_right can't be written this way, since it doesn't necessarily construct lists. I think it's also clear that we can write map2, flatten, split, and combine with setcdr (I like replace_tl as a name for this :) in tail recursive form, since map2, split, and combine are all tweaks of map, likewise flatten is a tweak of append. I just hacked up tail recursive versions of all of these functions (including the SML ones Minamide mentioned) using setcdr, so it is doable. PS: As you may know, you can certainly make functions like fold_right tail recursive using a trick called the CPS transformation, like so, from let rec fold_right f l accu = match l with [] -> accu | a::l -> f a (fold_right f l accu) to let rec fold_rightk f l accu k = match l with [] -> k accu | a::l -> fold_rightk f l accu (fun x -> k (f a x)) let fold_right f l accu = fold_rightk f l accu (fun x -> x) but as you can see that doesn't help so much, because we create lots of closures. So just making things tail recursive isn't really enough, since we can make anything tail recursive. This hole abstraction stuff Minamide discusses is a bit more, and touches on such areas as linear types. I think "destination passing style" will give you a few good hits on Google, if you're looking for more refs, but the Minamide paper cited earlier is a good start. -- Brian ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [Caml-list] @, List.append, and tail recursion 2003-01-31 17:13 ` Brian Hurt 2003-01-31 17:42 ` brogoff 2003-01-31 19:18 ` Russ Ross @ 2003-01-31 23:12 ` Issac Trotts 2 siblings, 0 replies; 16+ messages in thread From: Issac Trotts @ 2003-01-31 23:12 UTC (permalink / raw) To: OCaml Mailing List On Fri, Jan 31, 2003 at 11:13:26AM -0600, Brian Hurt wrote: > On Thu, 30 Jan 2003, james woodyatt wrote: > > > everyone-- > > > > Earlier in this thread, I suggested using a queue if you are spending > > too much time in List.append. Lack of optimized tail recursion is not > > really a factor compared to the waste of cycles involved in > > constructing a whole new list just to append a single element on the > > end. > > > > Apparently, nobody seemed to notice what I was talking about. So I'm > > going to try to make my point again. Sorry if you got it the first > > time. > > I did get it the first time. I'm just using List.append to illustrate a > problem I'm having. > > The problem is *constructing* lists. If you can construct your list > backwards, fine- but if you can't, you end up either not being tail > recursive (and blowing up for long lists) or allocating the list twice. > > Here's an example I have run across. I'm working with sparse vectors, and > basically storing them as (int * float) lists. Now, let's write the > vector add function. The naive implementation would be: > > let rec add x y = (* return x + y *) > match (x, y) with > ([], _) -> y > | (_, []) -> x > | (((xidx, xval) as xhead) :: xtail, > ((yidx, yval) as yhead) :: ytail) > -> > if (xidx == yidx) then > (xidx, xval +. yval) :: (add xtail ytail) > else if (xidx < yidx) then > xhead :: (add xtail y) > else > yhead :: (add x ytail) > ;; > > It's simple, and obvious in both what it does and how it does it. Except > opps, this isn't tail recursive. If your sparse vectors might be 65536 > elements long, this will blow up. So we rewrite to be tail recursive: > > let add x y = (* return x + y *) > let add_int x y accum = > match (x, y) with > ([], _) -> (List.rev_append accum y) > | (_, []) -> (List.rev_append accum x) > | (((xidx, xval) as xhead) :: xtail, > ((yidx, yval) as yhead) :: ytail) > -> > if (xidx == yidx) then > add_int xtail ytail ((xidx, xval +. yval) :: accum) > else if (xidx < yidx) then > add_int xtail y (xhead :: accum) > else > add_int x ytail (yhead :: accum) > ;; I get your meaning, but it has to be changed to something like this let add x y = (* return x + y *) let rec add_int x y accum = match (x, y) with ([], _) -> (List.rev_append accum y) | (_, []) -> (List.rev_append accum x) | (((xidx, xval) as xhead) :: xtail, ((yidx, yval) as yhead) :: ytail) -> if (xidx == yidx) then add_int xtail ytail ((xidx, xval +. yval) :: accum) else if (xidx < yidx) then add_int xtail y (xhead :: accum) else add_int x ytail (yhead :: accum) in add_int x y [] ;; to work on OCaml 3.06. > This makes the function truely tail recursive, except now it's allocating > most of the returned vector twice (once as accum, and once in > List.rev_append) and it's signifigantly uglier IMHO. Rewritting the code > to use set_cdr is the best performaner, but the ugliest yet. > > And the add function is truely simple. It can handle minor increases in > ugliness without losing much. But now consider something rather more > complicated- say matrix transposition or matrix multiplication with > matricies defined as: > > type vector_t = (int * float) list ;; (* index * value list *) > type matrix_t = (int * vector_t) list ;; (* row index * row vector list *) > > Now minor uglification becomes major uglification. It'd be nicer just to > be able to be able to construct lists forwards instead of backwards. Well, in your first example you are mapping ints to floats. Why not use a map for this? You are keeping the keys sorted, so using a map should be at least as good asymptotically when you're constructing it. module Imap = Map.Make(struct type t=int let compare=compare end);; let rec vec_make = function [] -> Imap.empty | (a,b) :: tail -> Imap.add a b (vec_make tail);; let a = vec_make [(0, 1.0); (3, 3.0)] ;; let b = vec_make [(1, 2.0); (3, 4.5); (4, 5.0)] ;; let vec_add x y = Imap.fold (fun index value acc -> try let acc_val = Imap.find index acc in let acc = Imap.remove index acc in Imap.add index (value +. acc_val) acc with Not_found -> Imap.add index value acc ) x y ;; let result = vec_add a b;; Imap.iter (fun i v -> Printf.printf "%i : %9.6f\n" i v) result;; 0 : 1.000000 1 : 2.000000 3 : 7.500000 4 : 5.000000 - : unit = () For matrices, how about let mat_add x y = Imap.fold (fun index value acc -> try let acc_val = Imap.find index acc in let acc = Imap.remove index acc in Imap.add index (vec_add value acc_val) acc with Not_found -> Imap.add index value acc ) x y ;; Issac > List.append is just an obvious example to be talking about, but the > problem is signifigantly more general. > > Brian > > > > > > > On Thursday, Jan 30, 2003, at 13:57 US/Pacific, Brian Hurt wrote: > > > On Thu, 30 Jan 2003, Olivier Andrieu wrote: > > > > > >>> list1: 1.462s > > >>> list2: 1.757s > > >>> list3: 1.824s > > >> > > >> There's an assert in setcdr : it's important because the first > > >> argument mustn't be an empty list. It's never the case here, so you > > >> can safely compile with -noassert. > > > > > > Doh! OK- now, compiling with -noassert drops the time to 1.457 seconds > > > (same machine, same environment)- to slightly better than the recursive > > > version. > > > > For grins, I wrote an equivalent test program. It uses a functional > > deque instead of a list. (I have written one. It's a component of my > > Cf library, which remains unreleased at the moment. Markus Mottl has > > translated several of Chris Okasaki's functional queue implementations > > into Ocaml, and you can find them on the Hump.) > > > > To get started, I timed the 'benchmarks' by running them on my iBook > > (the 700 MHz G3 model) so I could get a baseline. My little iBook is > > nowhere near as fast as your cool 1.6GHz P4, but I'm not complaining. > > > > The results of my tests were: > > > > $ time ./list1 > > 3.690u 0.070s 0:04.14 90.8% 0+0k 0+0io 0pf+0w > > > > $ time ./list2 > > 4.180u 0.020s 0:05.01 83.8% 0+0k 0+0io 0pf+0w > > > > $ time ./list3 > > 3.700u 0.000s 0:04.49 82.4% 0+0k 0+0io 0pf+0w > > > > Not real fast, but fast enough that I don't mind waiting for results. > > So, what difference does my functional deque implementation make? Glad > > you asked. > > > > My Cf_deque module matches the following signature: > > > > (* begin cf_deque.mli *) > > type 'a t > > > > val nil: 'a t > > > > module type Direction_T = sig > > val pop: 'a t -> ('a * 'a t) option > > val push: 'a -> 'a t -> 'a t > > end > > > > module A: Direction_T > > module B: Direction_T > > > > val cat: 'a t -> 'a t -> 'a t > > (* end file *) > > > > (Actually, this isn't the complete interface. I've written a variety > > of ancillary functions that make it convenient to work with the objects > > in a deque without having to translate them into lists, e.g. fold, > > iterate, etc.) > > > > All the functions above are purely functional, and they perform with > > O(1) average complexity. (Or at least, that's my untrained analysis. > > I'd like to provide proof of that assertion, and I'm working on getting > > some help in that effort-- but, I'll have more news on that when I have > > it.) > > > > Here's a variant of the list1.ml test above, which uses my Cf_deque > > module instead: > > > > (* begin t-opt.deque.ml *) > > open Cf_deque > > > > let rec makelist_aux c accum = > > let accum = B.push c accum in > > if c > 0 then makelist_aux (pred c) accum else accum > > > > let makelist c = makelist_aux c nil > > > > ;; > > let _ = makelist 5000;; > > (* end file *) > > > > Here are the timing results on that same iBook: > > > > $ time ./t-opt.deque > > 0.010u 0.010s 0:00.02 100.0% 0+0k 0+0io 0pf+0w > > > > In other words, it's done before enough time passes even to measure it > > properly. > > > > > And for the record, I just tested with appending to a list of 500,000 > > > elements, and it worked OK. > > > > Here is the result of running my version of the test with 500,000 > > elements: > > > > $ time ./t-opt.deque > > 0.450u 0.080s 0:00.75 70.6% 0+0k 0+0io 0pf+0w > > > > It took under a second of wall-clock time. On the other hand, when I > > modified list3.ml for 500,000 elements, it took *forever* in wall-clock > > time. I gave up after almost an hour and a half. Ultimately, I killed > > it with SIGINT before it finished. I have no idea how far it got. > > > > Clearly, I need to push a lot more elements into my deque before I will > > get a timing result that I can measure in heartbeats. Here is the > > result for 5,000,000 elements: > > > > $ time ./t-opt.deque > > 5.160u 0.510s 0:06.69 84.7% 0+0k 0+0io 0pf+0w > > > > At this stage, I noticed my little iBook was just starting to thrash > > when the program finished. So, I bumped it up to 50,000,000 > > elements, because I like punishing my computer from time to time-- just > > to teach it respect. > > > > At around 15 seconds into the run, the program turned into the > > psychedelic pizza delivery service: the pager went into fear and > > loathing mode, and the pretty Aqua GUI started acting like it was > > sniffing glue. If I had let it run to completion, it probably would > > have wedged the machine. (Mac OS X is pretty stable, but it hates > > resource bombs as much as any other operating system.) > > > > None of the listN.ml files were able to bring down the machine like > > that, no matter how long I let them run-- which should make sense, > > right? The APPEND function is O(N) for lists. Once N gets beyond a > > few hundred, the program spends almost all its cycles just copying list > > cells inside its innermost loop, only occasionally reaching the end of > > a list and tacking a new cell onto the end before starting over again. > > > > The problem is not the language, or the compiler. The problem is the > > design of the program. The moral of this story: you really should > > consider using a queue if you find your programs are spending a lot of > > cycles appending things to very long lists. > > > > > > > > ------------------- > To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr > Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ > Beginner's list: http://groups.yahoo.com/group/ocaml_beginners -- ------------------- To unsubscribe, mail caml-list-request@inria.fr Archives: http://caml.inria.fr Bug reports: http://caml.inria.fr/bin/caml-bugs FAQ: http://caml.inria.fr/FAQ/ Beginner's list: http://groups.yahoo.com/group/ocaml_beginners ^ permalink raw reply [flat|nested] 16+ messages in thread
end of thread, other threads:[~2003-02-01 10:19 UTC | newest] Thread overview: 16+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2003-01-24 0:48 [Caml-list] @, List.append, and tail recursion Brian Hurt 2003-01-30 18:10 ` Olivier Andrieu 2003-01-30 19:46 ` Brian Hurt 2003-01-30 20:52 ` Olivier Andrieu 2003-01-30 21:57 ` Brian Hurt 2003-01-31 2:16 ` james woodyatt 2003-01-31 17:05 ` Diego Olivier Fernandez Pons 2003-01-31 19:52 ` Brian Hurt 2003-02-01 10:18 ` Linear systems (was Re: [Caml-list] @, List.append, and tail recursion) Diego Olivier Fernandez Pons 2003-01-31 21:34 ` [Caml-list] @, List.append, and tail recursion Issac Trotts 2003-01-31 17:13 ` Brian Hurt 2003-01-31 17:42 ` brogoff 2003-01-31 19:18 ` Russ Ross 2003-01-31 19:32 ` Alexander V. Voinov 2003-02-01 2:30 ` brogoff 2003-01-31 23:12 ` Issac Trotts
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox