* counting words
@ 1999-01-19 16:47 Toby Moth
1999-01-19 22:52 ` Stefan Monnier
1999-01-20 2:01 ` Markus Mottl
0 siblings, 2 replies; 3+ messages in thread
From: Toby Moth @ 1999-01-19 16:47 UTC (permalink / raw)
To: caml-list
Remember the program in K & R for counting the number of occurrences of all the words in some input (K&R2, 6.5) ? Anyway, I happened to mention this program to a friend whilst a Perl hacker was listening in. To our surprise, the Perl programmer wrote a word counting program, including a binary tree sort, as we spoke. My friend then wrote his own rather less faithful but elegant version of this program in C++. Finally, I decided to write 2 OCAML versions - one using explicit buffering and streams, the other giving much of the work to ocamllex.
The surprise (at least for me) was this - both ocaml versions were significantly faster than the C++ solution. I think the Perl program wins on style. It is closest to the K&R solution and handles white space most cleverly. However, we all know that OCAML can be written stylishly. The point which I am only just beginning to appreciate is that OCAML is remarkably fast. After all, this is just the sort of program which people usually say should be written in C/C++ because it will then run faster.
Here are some results just to give a flavour of relative speeds on an Ultra 5.
[tm1]$ ls -l tm.dv
-rw-rw-r-- 1 tm1 this 452849 Jan 13 13:06 tm.dv
[tm1]$ time cat tm.dv | perl ~/lang/perl/wc.pl > pcount
real 0m28.604s PERL
user 0m28.260s
sys 0m0.100s
[tm1]$ time cat tm.dv | ~/lang/c/wc > ccount
real 0m2.708s C++
user 0m2.260s
sys 0m0.180s
[tm1]$ time cat tm.dv | ~/lang/caml/ywc > mcount
real 0m0.999s OCAML
user 0m0.720s
sys 0m0.080s
And here is the code -
PERL ( by Robin Barker )
---
package Tree;
sub new { bless { count => 0 } }
sub putintree {
my($tree,$value) = @_;
if( $tree->{count} ) {
if( $tree->{value} lt $value ) {
$tree->{up} -> putintree($value)
}
elsif( $tree->{value} gt $value ) {
$tree->{down} -> putintree($value)
}
else { $tree->{count}++ }
}
else {
$tree->{up} = new Tree;
$tree->{down} = new Tree;
$tree->{value} = $value;
$tree->{count} = 1;
}
}
sub show {
my $tree = shift;
if( $tree -> {count} ) {
$tree -> {down} -> show;
printf "%4d\t%s\n", $tree->{count}, $tree->{value};
$tree -> {up} -> show;
}
}
package main;
my $tree = new Tree;
while( <> ) { for ( /\w+/g ) { $tree-> putintree ($&) } }
$tree->show;
---
C++ ( by Steve Gardner, also an OCAML fan )
> Steve says:
> "This might be faster if it used lex"
---
#include <iostream>
#include <string>
#include <map>
int main()
{
string buf;
typedef map<string, long> mymap;
mymap vec;
while (cin >> buf) ++vec[buf];
for (mymap::const_iterator p = vec.begin(); p != vec.end(); ++p)
cout << p->first << ": " << p->second << endl;
}
---
OCAML ( by me. Forgive me any blemishes - I mean well )
---
(*** Lexer Approach to Word Count ***)
{
let hash = Hashtbl.create 101
let hash_add string =
try
let v = Hashtbl.find hash string in v:= !v + 1
with
Not_found -> Hashtbl.add hash string (ref 1)
}
rule read = parse
[' ' '\n' '\r' '\t']+ { read lexbuf }
| [^' ' '\n' '\r' '\r']+ { hash_add ( Lexing.lexeme lexbuf ); read lexbuf }
| eof { Hashtbl.iter ( fun k v -> Printf.printf "%4d\t%s\n" !v k ) hash }
{ let _ = read_rec ( Lexing.from_channel stdin ) 0 }
---
There are a few little extra details worth mentioning.
1) The 2nd OCAML program which I haven't included uses the sort of string buffering + stream approach which you can find in the genlex.ml code. This second approach is rather longer, but not much slower.
2) A longer C++ solution using #include <hash_map> was also much slower than either of the OCAML solutions.
3) The speed of the Perl program is irrelevant, since no hashing algorithm was being used.
--
Albert Tobias Moth
at work: research scientist at the National Physical Laboratory
tm1@npl.co.uk +44 (0)181 943 6016
at home: philosopher, dish washer, diarist, geek
toby@heynonnyno.demon.co.uk
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: counting words
1999-01-19 16:47 counting words Toby Moth
@ 1999-01-19 22:52 ` Stefan Monnier
1999-01-20 2:01 ` Markus Mottl
1 sibling, 0 replies; 3+ messages in thread
From: Stefan Monnier @ 1999-01-19 22:52 UTC (permalink / raw)
To: caml-list
>>>>> "Toby" == Toby Moth <tm1@cise.npl.co.uk> writes:
> [tm1]$ time cat tm.dv | perl ~/lang/perl/wc.pl > pcount
Running for 1999's "useless use of cat" contest ?
Stefan
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: counting words
1999-01-19 16:47 counting words Toby Moth
1999-01-19 22:52 ` Stefan Monnier
@ 1999-01-20 2:01 ` Markus Mottl
1 sibling, 0 replies; 3+ messages in thread
From: Markus Mottl @ 1999-01-20 2:01 UTC (permalink / raw)
To: Toby Moth; +Cc: OCAML
Hello,
> Here are some results just to give a flavour of relative speeds on an Ultra 5.
>
> [tm1]$ ls -l tm.dv
> -rw-rw-r-- 1 tm1 this 452849 Jan 13 13:06 tm.dv
> [tm1]$ time cat tm.dv | perl ~/lang/perl/wc.pl > pcount
>
> real 0m28.604s PERL
> user 0m28.260s
> sys 0m0.100s
> [tm1]$ time cat tm.dv | ~/lang/c/wc > ccount
>
> real 0m2.708s C++
> user 0m2.260s
> sys 0m0.180s
> [tm1]$ time cat tm.dv | ~/lang/caml/ywc > mcount
>
> real 0m0.999s OCAML
> user 0m0.720s
> sys 0m0.080s
Since comparing the performance of compilers is one of my hobbies ;-),
I have tried to follow this "contest" and have conducted a more thorough
test:
I implemented the solution to the problem with "flex" in C++, both with
maps and hash tables; then I wrote the PERL-version with hashes, too
(*very* short!) and then I took a look at the performance of the native
code compiler and interpreter of OCAML. The result of all runs had an
equal amount of characters (though, not in the same order, because hashes
don't sort). Thus, the implementation seems to be correct.
The test was conducted under Linux (PII 400Mhz, 128MB RAM), the
C++-compiler is "egcs-2.91.57" (optimizations: -O2 -fomit-frame-pointer).
The test case was the concatenated text of all the Linux HOWTO's on my
machine (about 7.3MB).
One of the problems with the C++-version (besides an internal compiler
error when trying to code the solution ;-) is that the number of buckets
in the hash table cannot be fixed at a low level. If the table is
created without parameters, it starts out with 193 buckets and increases
this number to 196613! To make up for this, I conducted the test under
OCAML with 101 buckets (as used by Albert) and the maximum found in C++
(196613). In another test I let C++ start out with this high level -
I think that we have a really fair comparison now.
Another problem with C++ was that finding the right use of templates was
everything but easy - the compiler not only sometimes emitted screen-fulls
of expanded templates as error message, but I'd say that the C++-code
looks really ill now! (I hope it's not my fault...)
By the way: I didn't use the STL-"string"-class for efficiency reasons...
Anyway, the speed result is very interesting indeed (in order from best to
worst, three successive tests each):
ocamlopt & ocamllex (196613 buckets):
4.270u 0.150s 0:04.42 100.0% 0+0k 0+0io 542pf+0w
4.250u 0.180s 0:04.66 95.0% 0+0k 0+0io 542pf+0w
4.240u 0.180s 0:04.42 100.0% 0+0k 0+0io 542pf+0w
ocaml (interpreted) & ocamllex (196613 buckets):
9.080u 0.180s 0:09.25 100.1% 0+0k 0+0io 539pf+0w
9.120u 0.150s 0:09.26 100.1% 0+0k 0+0io 539pf+0w
8.990u 0.270s 0:09.45 97.9% 0+0k 0+0io 539pf+0w
ocamlopt & ocamllex (101 buckets):
11.160u 0.270s 0:11.43 100.0% 0+0k 0+0io 542pf+0w
11.200u 0.240s 0:11.44 100.0% 0+0k 0+0io 542pf+0w
11.250u 0.200s 0:11.44 100.0% 0+0k 0+0io 542pf+0w
PERL:
11.340u 0.240s 0:11.58 100.0% 0+0k 0+0io 643pf+0w
11.380u 0.200s 0:11.58 100.0% 0+0k 0+0io 643pf+0w
11.350u 0.230s 0:11.58 100.0% 0+0k 0+0io 643pf+0w
C++ & flex (hash_map, 196613 buckets):
11.560u 0.340s 0:11.90 100.0% 0+0k 0+0io 552pf+0w
11.620u 0.280s 0:11.90 100.0% 0+0k 0+0io 552pf+0w
11.570u 0.320s 0:11.89 100.0% 0+0k 0+0io 552pf+0w
C++ & flex (hash_map, 193++ buckets):
11.780u 0.260s 0:12.04 100.0% 0+0k 0+0io 552pf+0w
11.720u 0.310s 0:12.03 100.0% 0+0k 0+0io 552pf+0w
11.870u 0.160s 0:12.03 100.0% 0+0k 0+0io 552pf+0w
C++ & flex (map):
14.530u 0.290s 0:14.82 100.0% 0+0k 0+0io 551pf+0w
14.540u 0.250s 0:14.80 99.9% 0+0k 0+0io 551pf+0w
14.630u 0.180s 0:14.81 100.0% 0+0k 0+0io 551pf+0w
ocaml (interpreted) & ocamllex (101 buckets):
20.450u 0.130s 0:20.58 100.0% 0+0k 0+0io 541pf+0w
20.300u 0.270s 0:20.57 100.0% 0+0k 0+0io 539pf+0w
20.450u 0.150s 0:20.59 100.0% 0+0k 0+0io 539pf+0w
The funny thing is that the OCAML-interpreter beats off all other
competitors (not the OCAML native code compiler, of course)!
Please tell this your local C++-guru :-)
Even PERL beats C++ without problem.
OCAML translated to native code is *way* faster than C++ (far more
than twice!)
The slowest competitor was the OCAML-interpreter with the unfair bucket
count of 101 - I just added it to show a result for a not unreasonable
number of buckets (196613 is probably a bit exaggerated...)
Now for the source code (shortest first):
PERL (yes, it *can* be this short!):
---------------------------------------------------------------------------
while(<>) { for (/[^ \t\n\r]+/g) { ++$table{$_}; } }
while (($key, $value) = each %table) { printf "%4d\t\%s\n", $value, $key; }
---------------------------------------------------------------------------
ocamllex-file (note: 196613 buckets):
---------------------------------------------------------------------------
{
let hash = Hashtbl.create 101
let hash_add string =
try
let v = Hashtbl.find hash string in v:= !v + 1
with
Not_found -> Hashtbl.add hash string (ref 1)
}
rule read = parse
[' ' '\n' '\r' '\t']+ { read lexbuf }
| [^' ' '\n' '\r' '\t']+ { hash_add ( Lexing.lexeme lexbuf ); read lexbuf }
| eof { Hashtbl.iter ( fun k v -> Printf.printf "%4d\t%s\n" !v k ) hash }
{ let _ = read_rec ( Lexing.from_channel stdin ) 0 }
---------------------------------------------------------------------------
flex and C++ with hash (note: 196613 buckets):
Don't forget to use option "-+" to get C++-code!
---------------------------------------------------------------------------
%option noyywrap
#include <stdio.h>
#include <string.h>
#include <hash_map>
struct eqstr
{
bool operator()(const char* s1, const char* s2) const
{
return strcmp(s1, s2) == 0;
}
};
typedef hash_map<const char*, int, hash<const char *>, eqstr > mymap;
mymap vec(196613);
%%
[^ \t\n\r]+ { char *foo = new char[yyleng + 1]; strcpy(foo, yytext);
++vec[foo]; }
[ \t\n\r]+ ;
%%
int main()
{
yyFlexLexer MyScanner;
MyScanner.yylex();
for (mymap::const_iterator p = vec.begin(); p != vec.end(); ++p)
printf ("%4d\t%s\n", p->second, p->first);
}
---------------------------------------------------------------------------
flex and C++ with map:
Don't forget to use option "-+" to get C++-code!
---------------------------------------------------------------------------
%option noyywrap
#include <stdio.h>
#include <string>
#include <map>
struct less_str
{
bool operator()(const char* s1, const char* s2) const
{
return strcmp(s1, s2) < 0;
}
};
typedef map<const char*, int, less_str> mymap;
mymap vec;
%%
[^ \t\n\r]+ { char *foo = new char[yyleng + 1]; strcpy(foo, yytext);
++vec[foo]; }
[ \t\n\r]+ ;
%%
int main()
{
yyFlexLexer MyScanner;
MyScanner.yylex();
for (mymap::const_iterator p = vec.begin(); p != vec.end(); ++p)
printf ("%4d\t%s\n", p->second, p->first);
}
---------------------------------------------------------------------------
So much about C++ and efficiency ;-)
Interesting to see that PERL did perform very well on this test. Its
brevity is unmatched! One shouldn't forget that its hash functions
are highly optimized, its regular expression matching capabilities,
as well. Though, it is definitely not a language for implementing data
structures, as we have seen in Albert's example.
I think that this test is really significant. I cannot imagine how to
make C++ so much faster that it approaches OCAML - without low-level
coding hash tables and pattern matching.
This test showed me, what a nice life I have since I use OCAML instead of
C++ (not only in terms of efficiency...)
Best regards,
Markus Mottl
--
Markus Mottl, mottl@miss.wu-wien.ac.at, http://miss.wu-wien.ac.at/~mottl
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~1999-01-20 13:02 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
1999-01-19 16:47 counting words Toby Moth
1999-01-19 22:52 ` Stefan Monnier
1999-01-20 2:01 ` Markus Mottl
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox