More Tcl socket performance

CL often has occasion to listen on TCP (and UDP, for that matter, but let's cover that later) ports for process control data. My general advice to clients is that they work in Tcl whenever possible, and that they not fret about performance in particular. While there's no question that C is "closer to the silicon", and is definitely faster for basic bit- and byte-wrangling, Tcl generally is plenty fast.

This page reports on one attempt to quantify that counsel. My model is a client which connects on a port, receives a sequence of bytes, twiddles their bits slightly, and computes a result. Here's the corresponding Tcl code:

    proc read_data {} {
        set sum 0
        set count 0
        set mask 0x5A
            # Choose any host other than the local one, and the difference 
            # between C and Tcl shrinks even more.  5837 is a port chosen 
            # merely for convenience.
        set channel [socket localhost 5837]
        fconfigure $channel -translation binary
        while 1 {
            set data [read $channel]
            foreach item [split $data {}] {
                incr count
                    # In process-control contexts, I often need to mask 
                    # off a few bits and manipulate the resulting value.  
                    # Neither $mask nor the single right-shift are 
                    # particularly meaningful; they're just the results 
                    # of experiments to yield suggestive, quantifiable 
                    # results. 
                set value [scan $item %c]
                set addend [expr {($mask & $value) >> 1}]
                incr sum $addend
                # puts -nonewline "$value ($addend) "
            }
            if {[eof $channel]} break
        }
        puts "\nSum is '[format %x $sum]'."
        return $count
    }

    set result [time {set count [read_data]} 3]
    puts $result
        # This is a hackish way to extract the time.
    set microseconds [lindex $result 0]
    set quotient [expr round(double($microseconds) / $count)]
    puts "$quotient microseconds per byte received."

On a generic Linux x86 host at hand, when I blasted random data from a simple server (see below) as fast as possible, I observed these results. The latter two columns are microseconds per byte received:

   Bytes  Tcl   C
  ------  ---  ---
     100   17   36
     300   11   14
    1000    8    6
   10000    6    2
   30000    6    2
  100000    6    2      

My summary: don't choose C because of performance. Unless message sizes are quite large, or you're willing to tune your C coding carefully, you can safely assume that Tcl's performance penalty is 60% at most (an amount easily lost in network, UI, and other noise), and possibly far less.


Here's a simple C recv-based client:

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netdb.h>
    #include <sys/time.h>

    void error(char *msg)
    {
        perror(msg);
        exit(0);
    }


    long int difference(struct timeval *before, struct timeval *after)
    {
        return 1000000 * (after->tv_sec - before->tv_sec) +
                         (after->tv_usec - before->tv_usec);
    }

    int main(int argc, char *argv[])
    {
        int sockfd, portno, n, byte, rc;
        int mask, sum, count, diff, addend;
        char *ptr;
        struct sockaddr_in serv_addr;
        struct hostent *server;
        unsigned char buf[512];
        struct timeval before, after;

        sum = 0;
        count = 0;
        portno = 5837;
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
            error("ERROR opening socket");
        server = gethostbyname("localhost");
        if (server == NULL) {
            fprintf(stderr,"ERROR, no such host\n");
            exit(0);
        }
        bzero((char *) &serv_addr, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        bcopy((char *)server->h_addr,
             (char *)&serv_addr.sin_addr.s_addr,
             server->h_length);
        serv_addr.sin_port = htons(portno);
        if (connect(sockfd, &serv_addr,sizeof(serv_addr)) < 0)
            error("ERROR connecting");
        mask = 0x5A;
        gettimeofday(&before, NULL);
        while (1) {
            rc = recv(sockfd, buf, sizeof(buf), 0);
            if (rc <= 0) break;
            for (ptr = buf; rc--;) { 
                byte = *ptr++;
                count++;
                addend = (mask & byte) >> 1;
                sum += addend;
            /* printf("%d (%d) ", byte, addend); */
            }
        }
        printf("\nSum is '%x'.\n", sum);
        gettimeofday(&after, NULL);
        printf("%ld and %ld.\n", after.tv_sec, after.tv_usec);
        diff = difference(&before, &after);
        printf("Elapsed time is %d microseconds.\n", diff);
        printf("That is %d microseconds per byte received.\n", diff / count);
        return 0;
    }

[Still to do: exhibit model server, and comment on Windows vs. Unix.]


See also: