[Ns-developers] [ns3] socket API

Mathieu Lacage mathieu.lacage at sophia.inria.fr
Tue Apr 15 10:48:18 PDT 2008


On Tue, 2008-04-15 at 17:10 +0000, Tom Henderson wrote:

> If I understand correctly, the efficiency goal here is to try to avoid
> buffer copies at the various APIs.

The goal is to avoid buffer copies _and_ memory allocations for "fake"
payload. The latter is actually _the_ critical constraint for the
use-case sally described in our first meeting in berkeley because memory
allocations for unused payload will quickly dominate and bound fast-tcp
simulations.

> 
> In the send direction, whether Send (Ptr<Packet>) or Send (const
> uint8_t* buf) is used does not matter; one copy into the packet buffer
> is performed in either case, and a fake buffer is supported similarly
> in either case.

No, the two cases are different:

// no memory allocated for payload.
Ptr<Packet> p = Create<Packet> (100000);
socket->Send (p);

Socket::Send (Ptr<const Packet> p)
{
  // copies just the packet data structure: does not allocate 
  // memory for the payload.
  m_list.push_back (p->Copy ());
}

and the second case:

uint8_t buffer[100000];
memset (buffer, 100000, 0);
socket->Send (buffer);
Socket::Send (uint8_t *buffer)
{
  // allocate a packet buffer of size 100000 and copy zeros in it.
  Ptr<Packet> p = Create<Packet> (buffer);
  m_list.push_back (p);
}

So, in the second case, you:

  - always allocate a buffer for the payload
  - always memcpy it
  - have to memset it.

while in the first case, none of the above happens.


> In the receive direction, if you want to support recv/recvfrom
> semantics, the choices are Recv (..., Ptr<Packet>, ...) or Recv (...,
> uint8_t* buf, ...).
> 
> In the case of fake data, there is no efficiency difference because
> zero bytes are copied.  In the case of real data, the use of the buf

There is a difference, even in the case of fake data: If you have fake
data, Recv (uint8_t *buf) will require that you actually convert that
fake data to a real buffer and copy it. The Ptr<Packet> version, on the
other hand, will not require that last conversion and will track of the
fake data without allocating it.

To be clear, an example:

// don't allocate anything.
Ptr<Packet> p = 0;

p = socket->Recv (10);

Ptr<Packet> Socket::Recv (uint32_t maxSize)
{
  Ptr<Packet> p = m_list.front ();
  if (p->GetSize () > maxSize)
  {
    // must split next packet.
    Ptr<Packet> left = p->CreateFragment (maxSize, p->GetSize () -
maxSize);
    m_list.pop_front ();
    m_list.push_front (left);
    // return the requested size.
    return p->CreateFragment (maxSize);
  } 
  else
  {
    // no need to split.
    m_list.pop_front ();
    return p->Copy ();
  } 
}

None of the above allocates _any_ buffer for the payload if it was fake
because CreateFragment is smart enough to not to it. That, however,
clearly assumes that you have used Packet::Packet (uint32_t size) to
create the original payload packet, that is, that you have used
Socket::Send (Ptr<Packet>) and not the Send (uint8_t *buffer) version.

On the other hand, the following:

uint8_t buffer[1024];

int recved = socket->Recv (buffer, 1024);

int
Socket::Recv (uint8_t *buffer, uint32_t size)
{
  Ptr<Packet> p = Recv (size);
  uint8_t *buf = p->PeekData ();
  memcpy (buffer, buf, p->GetSize ());
  return p->GetSize ();
}

will require that you:
  - perform an extra memcpy
  - if the data in the packet was fake, PeekData will make that fake
data become real so, even if the sending side was super-careful to
allocate payload with Packet::Packet (uint32_t size), then, you lose
here because you end up actually allocating a buffer for the data
anyway.

>  version requires a copy from Packet to buf.  If you use the Recv
> (Ptr<Packet>) version, you can avoid this copy if you use
> Packet::PeekData().  Is this what you mean by _really_ efficient

PeekData will trigger a real data buffer allocation. As soon as you
start using a real byte buffer, the fake data becomes real. Example:

// no buffer allocated for payload.
Ptr<Packet> p = Create<Packet> (100000);

// buffer magically allocated: fake data has become real zeros.
uint8_t *buf = p->PeekData ();

>  synchronous applications?   Otherwise, it doesn't seem like the two
> variants differ in efficiency, for the common case.
> 
> There are also cases in which the Recv does not align with the Packet
> boundaries as Packets were delivered to the receive socket.  For
> instance, in TCP, you may have a read that spans multiple packet (and
> packet Buffers).  The application may also read less than what is
> available.  In these cases, do you end up losing the _really_
> efficient operation of the Ptr<Packet> version, when your read spans
> underlying Buffers?  

No: In the case of fake data stored in a Ptr<Packet>, Ptr<Packet> is
still smart enough to keep track of slices of the fake data if it is
sliced.

> 
> In the case of a large read, wouldn't you have to create a new Packet
> that concatenated the underlying Buffers (and hence required a copy
> into this new Buffer)?  If not, how would that work?

If you kept track of the received data with a list of pointers to
packets, you can aggregate them with Packet::AddAtEnd and that will also
be smart enough to avoid buffer allocations in case of fake data.

> 
> In the case of the small read, you may have multiple Packet objects
> pointing at the same Buffer, with differing offsets (m_start, m_end),
> so unless the read spans Buffers, then it seems like you can also
> avoid copies here. 

I am sorry but I do not understand what you mean by this.

> 
> So, in summary, it seems to me that the real issue is that in the
> receive direction, you may be able to get by in many (but not all)
> cases by effectively passing a pointer (to const) to the underlying
> Buffer up to the application, since the smart pointer semantics of
> Packet make this a safe operation.  But if you use the "const uint8_t*
> buf" variant of recv, you always must copy into buf, from a memory
> management perspective.  Is this an accurate summary?

I don't think so. As soon as you start manipulating a raw buffer, you
have to populate it with data, whether it is just a bunch of fake zeros
or not. So, you have allocate the buffer to be big enough to contain
that data, hence, you lose from a memory-management perspective. If,
instead, you use a Ptr<Packet> with fake data, you never allocate the
buffer:

// 100 KB of fake data.
Ptr<Packet> p = Create<Packet> (100000);

// still 10KB of fake data.
Ptr<Packet> a = p.CreateFrament (10000);

// still 90KB of fake data.
Ptr<Packet> b = p.CreateFrament (90000);

// 100KB of fake data is back.
a->AddAtEnd (b);

All of that fancy super-efficient lack of memory allocation was
implemented to support the one case sally cared about, that, is being
able to deal with as many fast-tcp streams as humanly possible without
overflowing the system with memory allocations for the payload which,
for her use-cases, was considered useless.

So, as far as I can tell, a better summary of the issues is that, if you
use the API I proposed consistently in applications and in the socket
code, you will be able to avoid:
  - allocating buffers for fake payload
  - copying the fake payload around

but, if you put anywhere in the source or destination code paths a real
raw buffer, you will have to convert your data to a raw buffer and that
will trigger extra memcpys as well as extra memory allocations because
the fake data will become real.

Mathieu



More information about the Ns-developers mailing list