In fact, the implementor should be sensitive to the possibility of
even more peculiar slicing strategies in dividing up the various
protocol layers between the kernel and the one or more user processes.
The result of the strategy proposed above was that part of TCP should
execute in the process of the user. In other words, instead of having
one TCP process for the system, there is one TCP process per connection.
Given this architecture, it is not longer necessary to imagine that all
of the TCPs are identical. One TCP could be optimized for high
throughput applications, such as file transfer. Another TCP could be
optimized for small low delay applications such as Telnet. In fact, it
would be possible to produce a TCP which was somewhat integrated with
the Telnet or FTP on top of it. Such an integration is extremely
important, for it can lead to a kind of efficiency which more
traditional structures are incapable of producing. Earlier, this paper
pointed out that one of the important rules to achieving efficiency was
to send the minimum number of packets for a given amount of data. The
idea of protocol layering interacts very strongly (and poorly) with this
goal, because independent layers have independent ideas about when
14
packets should be sent, and unless these layers can somehow be brought
into cooperation, additional packets will flow. The best example of
this is the operation of server telnet in a character at a time remote
echo mode on top of TCP. When a packet containing a character arrives
at a server host, each layer has a different response to that packet.
TCP has an obligation to acknowledge the packet. Either server telnet
or the application layer above has an obligation to echo the character
received in the packet. If the character is a Telnet control sequence,
then Telnet has additional actions which it must perform in response to
the packet. The result of this, in most implementations, is that
several packets are sent back in response to the one arriving packet.
Combining all of these return messages into one packet is important for
several reasons. First, of course, it reduces the number of packets
being sent over the net, which directly reduces the charges incurred for
many common carrier tariff structures. Second, it reduces the number of
scheduling actions which will occur inside both hosts, which, as was
discussed above, is extremely important in improving throughput.
The way to achieve this goal of packet sharing is to break down the
barrier between the layers of the protocols, in a very restrained and
careful manner, so that a limited amount of information can leak across
the barrier to enable one layer to optimize its behavior with respect to
the desires of the layers above and below it. For example, it would
represent an improvement if TCP, when it received a packet, could ask
the layer above whether or not it would be worth pausing for a few
milliseconds before sending an acknowledgement in order to see if the
upper layer would have any outgoing data to send. Dallying before
15
sending the acknowledgement produces precisely the right sort of
optimization if the client of TCP is server Telnet. However, dallying
before sending an acknowledgement is absolutely unacceptable if TCP is
being used for file transfer, for in file transfer there is almost never
data flowing in the reverse direction, and the delay in sending the
acknowledgement probably translates directly into a delay in obtaining
the next packets. Thus, TCP must know a little about the layers above
it to adjust its performance as needed.
It would be possible to imagine a general purpose TCP which was
equipped with all sorts of special mechanisms by which it would query
the layer above and modify its behavior accordingly. In the structures
suggested above, in which there is not one but several TCPs, the TCP can
simply be modified so that it produces the correct behavior as a matter
of course. This structure has the disadvantage that there will be
several implementations of TCP existing on a single machine, which can
mean more maintenance headaches if a problem is found where TCP needs to
be changed. However, it is probably the case that each of the TCPs will
be substantially simpler than the general purpose TCP which would
otherwise have been built. There are some experimental projects
currently under way which suggest that this approach may make designing
of a TCP, or almost any other layer, substantially easier, so that the
total effort involved in bringing up a complete package is actually less
if this approach is followed. This approach is by no means generally
accepted, but deserves some consideration.
16
The general conclusion to be drawn from this sort of consideration
is that a layer boundary has both a benefit and a penalty. A visible
layer boundary, with a well specified interface, provides a form of
isolation between two layers which allows one to be changed with the
confidence that the other one will not stop working as a result.
However, a firm layer boundary almost inevitably leads to inefficient
operation. This can easily be seen by analogy with other aspects of
operating systems. Consider, for example, file systems. A typical
operating system provides a file system, which is a highly abstracted
representation of a disk. The interface is highly formalized, and
presumed to be highly stable. This makes it very easy for naive users
to have access to disks without having to write a great deal of
software. The existence of a file system is clearly beneficial. On the
other hand, it is clear that the restricted interface to a file system
almost inevitably leads to inefficiency. If the interface is organized
as a sequential read and write of bytes, then there will be people who
wish to do high throughput transfers who cannot achieve their goal. If
the interface is a virtual memory interface, then other users will
regret the necessity of building a byte stream interface on top of the
memory mapped file. The most objectionable inefficiency results when a
highly sophisticated package, such as a data base management package,
must be built on top of an existing operating system. Almost
inevitably, the implementors of the database system attempt to reject
the file system and obtain direct access to the disks. They have
sacrificed modularity for efficiency.
The same conflict appears in networking, in a rather extreme form.
17
The concept of a protocol is still unknown and frightening to most naive
programmers. The idea that they might have to implement a protocol, or
even part of a protocol, as part of some application package, is a
dreadful thought. And thus there is great pressure to hide the function
of the net behind a very hard barrier. On the other hand, the kind of
inefficiency which results from this is a particularly undesirable sort
of inefficiency, for it shows up, among other things, in increasing the
cost of the communications resource used up to achieve the application
goal. In cases where one must pay for one's communications costs, they
usually turn out to be the dominant cost within the system. Thus, doing
an excessively good job of packaging up the protocols in an inflexible
manner has a direct impact on increasing the cost of the critical
resource within the system. This is a dilemma which will probably only
be solved when programmers become somewhat less alarmed about protocols,
so that they are willing to weave a certain amount of protocol structure
into their application program, much as application programs today weave
parts of database management systems into the structure of their
application program.
An extreme example of putting the protocol package behind a firm
layer boundary occurs when the protocol package is relegated to a front-
end processor. In this case the interface to the protocol is some other
protocol. It is difficult to imagine how to build close cooperation
between layers when they are that far separated. Realistically, one of
the prices which must be associated with an implementation so physically
modularized is that the performance will suffer as a result. Of course,
a separate processor for protocols could be very closely integrated into
the mainframe architecture, with interprocessor co-ordination signals,
18
shared memory, and similar features. Such a physical modularity might
work very well, but there is little documented experience with this
closely coupled architecture for protocol support.
6. Efficiency of Protocol Processing
To this point, this document has considered how a protocol package
should be broken into modules, and how those modules should be
distributed between free standing machines, the operating system kernel,
and one or more user processes. It is now time to consider the other
half of the efficiency question, which is what can be done to speed the
execution of those programs that actually implement the protocols. We
will make some specific observations about TCP and IP, and then conclude
with a few generalities.
IP is a simple protocol, especially with respect to the processing
of normal packets, so it should be easy to get it to perform
efficiently. The only area of any complexity related to actual packet
processing has to do with fragmentation and reassembly. The reader is
referred to RFC 815, titled "IP Datagram Reassembly Algorithms", for
specific consideration of this point.
Most costs in the IP layer come from table look up functions, as
opposed to packet processing functions. An outgoing packet requires two
translation functions to be performed. The internet address must be
translated to a target gateway, and a gateway address must be translated
to a local network number (if the host is attached to more than one
network). It is easy to build a simple implementation of these table
19
look up functions that in fact performs very poorly. The programmer
should keep in mind that there may be as many as a thousand network
numbers in a typical configuration. Linear searching of a thousand
entry table on every packet is extremely unsuitable. In fact, it may be
worth asking TCP to cache a hint for each connection, which can be
handed down to IP each time a packet is sent, to try to avoid the
overhead of a table look up.
TCP is a more complex protocol, and presents many more
opportunities for getting things wrong. There is one area which is
generally accepted as causing noticeable and substantial overhead as
part of TCP processing. This is computation of the checksum. It would
be nice if this cost could be avoided somehow, but the idea of an end-
to-end checksum is absolutely central to the functioning of TCP. No
host implementor should think of omitting the validation of a checksum
on incoming data.
Various clever tricks have been used to try to minimize the cost of
computing the checksum. If it is possible to add additional microcoded
instructions to the machine, a checksum instruction is the most obvious
candidate. Since computing the checksum involves picking up every byte
of the segment and examining it, it is possible to combine the operation
of computing the checksum with the operation of copying the segment from
one location to another. Since a number of data copies are probably
already required as part of the processing structure, this kind of
sharing might conceivably pay off if it didn't cause too much trouble to
the modularity of the program. Finally, computation of the checksum
20
seems to be one place where careful attention to the details of the
algorithm used can make a drastic difference in the throughput of the
program. The Multics system provides one of the best case studies of
this, since Multics is about as poorly organized to perform this
function as any machine implementing TCP. Multics is a 36-bit word
machine, with four 9-bit bytes per word. The eight-bit bytes of a TCP
segment are laid down packed in memory, ignoring word boundaries. This
means that when it is necessary to pick up the data as a set of 16-bit
units for the purpose of adding them to compute checksums, horrible
masking and shifting is required for each 16-bit value. An early
version of a program using this strategy required 6 milliseconds to
checksum a 576-byte segment. Obviously, at this point, checksum
computation was becoming the central bottleneck to throughput. A more
careful recoding of this algorithm reduced the checksum processing time
to less than one millisecond. The strategy used was extremely dirty.
It involved adding up carefully selected words of the area in which the
data lay, knowing that for those particular words, the 16-bit values
were properly aligned inside the words. Only after the addition had
been done were the various sums shifted, and finally added to produce
the eventual checksum. This kind of highly specialized programming is
probably not acceptable if used everywhere within an operating system.
It is clearly appropriate for one highly localized function which can be
clearly identified as an extreme performance bottleneck.
Another area of TCP processing which may cause performance problems
is the overhead of examining all of the possible flags and options which
occur in each incoming packet. One paper, by Bunch and Day [2], asserts
21
that the overhead of packet header processing is actually an important
limiting factor in throughput computation. Not all measurement
experiments have tended to support this result. To whatever extent it
is true, however, there is an obvious strategy which the implementor
ought to use in designing his program. He should build his program to
optimize the expected case. It is easy, especially when first designing
a program, to pay equal attention to all of the possible outcomes of
every test. In practice, however, few of these will ever happen. A TCP
should be built on the assumption that the next packet to arrive will
have absolutely nothing special about it, and will be the next one
expected in the sequence space. One or two tests are sufficient to
determine that the expected set of control flags are on. (The ACK flag
should be on; the Push flag may or may not be on. No other flags should
be on.) One test is sufficient to determine that the sequence number of
the incoming packet is one greater than the last sequence number
received. In almost every case, that will be the actual result. Again,
using the Multics system as an example, failure to optimize the case of
receiving the expected sequence number had a detectable effect on the
performance of the system. The particular problem arose when a number
of packets arrived at once. TCP attempted to process all of these
packets before awaking the user. As a result, by the time the last
packet arrived, there was a threaded list of packets which had several
items on it. When a new packet arrived, the list was searched to find
the location into which the packet should be inserted. Obviously, the
list should be searched from highest sequence number to lowest sequence
number, because one is expecting to receive a packet which comes after
22
those already received. By mistake, the list was searched from front to
back, starting with the packets with the lowest sequence number. The
amount of time spent searching this list backwards was easily detectable
in the metering measurements.
Other data structures can be organized to optimize the action which
is normally taken on them. For example, the retransmission queue is
very seldom actually used for retransmission, so it should not be
organized to optimize that action. In fact, it should be organized to
optimized the discarding of things from it when the acknowledgement
arrives. In many cases, the easiest way to do this is not to save the
packet at all, but to reconstruct it only if it needs to be
retransmitted, starting from the data as it was originally buffered by
the user.
There is another generality, at least as important as optimizing
the common case, which is to avoid copying data any more times than
necessary. One more result from the Multics TCP may prove enlightening
here. Multics takes between two and three milliseconds within the TCP
layer to process an incoming packet, depending on its size. For a 576-
byte packet, the three milliseconds is used up approximately as follows.
One millisecond is used computing the checksum. Six hundred
microseconds is spent copying the data. (The data is copied twice, at
.3 milliseconds a copy.) One of those copy operations could correctly
be included as part of the checksum cost, since it is done to get the
data on a known word boundary to optimize the checksum algorithm.
However, the copy also performs another necessary transfer at the same
23
time. Header processing and packet resequencing takes .7 milliseconds.
The rest of the time is used in miscellaneous processing, such as
removing packets from the retransmission queue which are acknowledged by
this packet. Data copying is the second most expensive single operation
after data checksuming. Some implementations, often because of an
excessively layered modularity, end up copying the data around a great
deal. Other implementations end up copying the data because there is no
shared memory between processes, and the data must be moved from process
to process via a kernel operation. Unless the amount of this activity
is kept strictly under control, it will quickly become the major
performance bottleneck.
7. Conclusions
This document has addressed two aspects of obtaining performance
from a protocol implementation, the way in which the protocol is layered
and integrated into the operating system, and the way in which the
detailed handling of the packet is optimized. It would be nice if one
or the other of these costs would completely dominate, so that all of
one's attention could be concentrated there. Regrettably, this is not
so. Depending on the particular sort of traffic one is getting, for
example, whether Telnet one-byte packets or file transfer maximum size
packets at maximum speed, one can expect to see one or the other cost
being the major bottleneck to throughput. Most implementors who have
studied their programs in an attempt to find out where the time was
going have reached the unsatisfactory conclusion that it is going
equally to all parts of their program. With the possible exception of
24
checksum processing, very few people have ever found that their
performance problems were due to a single, horrible bottleneck which
they could fix by a single stroke of inventive programming. Rather, the
performance was something which was improved by painstaking tuning of
the entire program.
Most discussions of protocols begin by introducing the concept of
layering, which tends to suggest that layering is a fundamentally
wonderful idea which should be a part of every consideration of
protocols. In fact, layering is a mixed blessing. Clearly, a layer
interface is necessary whenever more than one client of a particular
layer is to be allowed to use that same layer. But an interface,
precisely because it is fixed, inevitably leads to a lack of complete
understanding as to what one layer wishes to obtain from another. This
has to lead to inefficiency. Furthermore, layering is a potential snare
in that one is tempted to think that a layer boundary, which was an
artifact of the specification procedure, is in fact the proper boundary
to use in modularizing the implementation. Again, in certain cases, an
architected layer must correspond to an implemented layer, precisely so
that several clients can have access to that layer in a reasonably
straightforward manner. In other cases, cunning rearrangement of the
implemented module boundaries to match with various functions, such as
the demultiplexing of incoming packets, or the sending of asynchronous
outgoing packets, can lead to unexpected performance improvements
compared to more traditional implementation strategies. Finally, good
performance is something which is difficult to retrofit onto an existing
program. Since performance is influenced, not just by the fine detail,
25
but by the gross structure, it is sometimes the case that in order to
obtain a substantial performance improvement, it is necessary to
completely redo the program from the bottom up. This is a great
disappointment to programmers, especially those doing a protocol
implementation for the first time. Programmers who are somewhat
inexperienced and unfamiliar with protocols are sufficiently concerned
with getting their program logically correct that they do not have the
capacity to think at the same time about the performance of the
structure they are building. Only after they have achieved a logically
correct program do they discover that they have done so in a way which
has precluded real performance. Clearly, it is more difficult to design
a program thinking from the start about both logical correctness and
performance. With time, as implementors as a group learn more about the
appropriate structures to use for building protocols, it will be
possible to proceed with an implementation project having more
confidence that the structure is rational, that the program will work,
and that the program will work well. Those of us now implementing
protocols have the privilege of being on the forefront of this learning
process. It should be no surprise that our programs sometimes suffer
from the uncertainty we bring to bear on them.
Citations
26
[1] Cohen and Postel, "On Protocol Multiplexing", Sixth Data
Communications Symposium, ACM/IEEE, November 1979.
[2] Bunch and Day, "Control Structure Overhead in TCP", Trends and
Applications: Computer Networking, NBS Symposium, May 1980.
Ad
Translate documents to 日本語, svenska, Nederlands, Deutsch, français, русский, italiano, español, Tiếng Việt, polski, português, 中文, українська, català, norsk, فارسی, suomi, Bahasa Indonesia, العربية, čeština, 한국어, Bahasa Melayu, magyar, română, српски and other languages.