10

Today's problem is that I need to write an array of numbers in a binary file at a starting position. I have the position where it should start, and I don't want to overwrite values after that, just want to insert the array at the starting position in the file. E.g:

12345

Let's push 456 at position 2:

12456345

I know that probably I'll have to implement it by myself, but I want to know what's your opinion on how to implement that as efficiently as possible.

tshepang
  • 12,111
  • 21
  • 91
  • 136
Frederico Schardong
  • 1,946
  • 6
  • 38
  • 62
  • @John's answer seems like it may be the only way but it will involve lots of copying for large files. So if at all possible, looking for another approach to data serialization may be the best thing to do. – gcbenison May 06 '12 at 03:33
  • @gcbenison, yes, my binary file can be 1GB and the enlargement process will be triggered a lot of time so this probably will be a problem – Frederico Schardong May 06 '12 at 03:41
  • It would be best if you can avoid having to insert data into the middle of a file, because it is so expensive to do, doubly so in gigabyte size files. – Jonathan Leffler May 06 '12 at 03:55
  • @JonathanLeffler yea you're right. Maybe is better just store in small files and then when I don't need to enlarge it anymore then just concatenate them. – Frederico Schardong May 06 '12 at 04:02
  • 2
    Absolutely make sure to have good tests for this insertion code. Cover small offsets and sizes, edge cases and large offsets and sizes. The latter is especially important because you cannot afford arithmetic overflows when handling large files. Those will result in hangs, crashes, files that are too short or too long compared to what they should be and corrupted file data. 32-bit ints will start overflowing at ~2GB. – Alexey Frunze May 06 '12 at 05:23

4 Answers4

13

Here's a function extend_file_and_insert() that does the job, more or less.

#include <sys/stat.h>
#include <unistd.h>

enum { BUFFERSIZE = 64 * 1024 };

#define MIN(x, y) (((x) < (y)) ? (x) : (y))

/*
off_t   is signed
ssize_t is signed
size_t  is unsigned

off_t   for lseek() offset and return
size_t  for read()/write() length
ssize_t for read()/write() return
off_t   for st_size
*/

static int extend_file_and_insert(int fd, off_t offset, char const *insert, size_t inslen)
{
    char buffer[BUFFERSIZE];
    struct stat sb;
    int rc = -1;

    if (fstat(fd, &sb) == 0)
    {
        if (sb.st_size > offset)
        {
            /* Move data after offset up by inslen bytes */
            size_t bytes_to_move = sb.st_size - offset;
            off_t read_end_offset = sb.st_size; 
            while (bytes_to_move != 0)
            {
                ssize_t bytes_this_time = MIN(BUFFERSIZE, bytes_to_move);
                ssize_t rd_off = read_end_offset - bytes_this_time;
                ssize_t wr_off = rd_off + inslen;
                lseek(fd, rd_off, SEEK_SET);
                if (read(fd, buffer, bytes_this_time) != bytes_this_time)
                    return -1;
                lseek(fd, wr_off, SEEK_SET);
                if (write(fd, buffer, bytes_this_time) != bytes_this_time)
                    return -1;
                bytes_to_move -= bytes_this_time;
                read_end_offset -= bytes_this_time; /* Added 2013-07-19 */
            }   
        }   
        lseek(fd, offset, SEEK_SET);
        write(fd, insert, inslen);
        rc = 0;
    }   
    return rc;
}

(Note the additional line added 2013-07-19; it was a bug that only shows when the buffer size is smaller than the amount of data to be copied up the file. Thanks to malat for pointing out the error. Code now tested with BUFFERSIZE = 4.)

This is some small-scale test code:

#include <fcntl.h>
#include <string.h>

static const char base_data[] = "12345";
typedef struct Data
{
    off_t       posn;
    const char *data;
} Data;
static const Data insert[] =
{
    {  2, "456"                       },
    {  4, "XxxxxxX"                   },
    { 12, "ZzzzzzzzzzzzzzzzzzzzzzzzX" },
    { 22, "YyyyyyyyyyyyyyyY"          },
};  
enum { NUM_INSERT = sizeof(insert) / sizeof(insert[0]) };

int main(void)
{
    int fd = open("test.dat", O_RDWR | O_TRUNC | O_CREAT, 0644);
    if (fd > 0)
    {
        ssize_t base_len = sizeof(base_data) - 1;
        if (write(fd, base_data, base_len) == base_len)
        {
            for (int i = 0; i < NUM_INSERT; i++)
            {
                off_t length = strlen(insert[i].data);
                if (extend_file_and_insert(fd, insert[i].posn, insert[i].data, length) != 0)
                    break;
                lseek(fd, 0, SEEK_SET);
                char buffer[BUFFERSIZE];
                ssize_t nbytes;
                while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
                    write(1, buffer, nbytes);
                write(1, "\n", 1);
            }
        }
        close(fd);
    }
    return(0);
}

It produces the output:

12456345
1245XxxxxxX6345
1245XxxxxxX6ZzzzzzzzzzzzzzzzzzzzzzzzZ345
1245XxxxxxX6ZzzzzzzzzzYyyyyyyyyyyyyyyYzzzzzzzzzzzzzzZ345

It should be tested on some larger files (ones bigger than BUFFERSIZE, but it would be sensible to test with a BUFFERSIZE a lot smaller than 64 KiB; I used 32 bytes and it seemed to be OK). I've only eyeballed the results but the patterns are designed to make it easy to see that they are correct. The code does not check any of the lseek() calls; that's a minor risk.

Community
  • 1
  • 1
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • @malat: Yes, you're right. When the code needs to do more than one buffer-full of moving data, the value of `read_end_offset` needs to be decremented by `bytes_this_time`. I've code the fix and tested with `BUFFERSIZE=4` (which showed the bug on the unfixed code). Thanks for pointing that bug out. – Jonathan Leffler Jul 19 '13 at 17:39
5

First, use ftruncate() to enlarge the file to the final size. Then copy everything from the old end over to the new end, working your way back to the insertion point. Then overwrite the middle contents with the data you want to insert. This is as efficient as it gets, I think, because filesystems don't generally offer true "insertion" in the middle of files.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
1

I'm agreeing with the others, but let me state the solution a bit differently:

  1. Get a temp filename (there are OS-specific calls for this)

  2. Copy your original file to the temp file (there are now two copies of the same file)

  3. Open the original file for "append".

  4. "Truncate" it to your insertion point

  5. Write your new data

  6. Open your temp file for "read"

  7. "Seek" to the insertion point (again, the call is OS-specific)

  8. Read to end-of-file in temp file; inserting into your original file (still open for "append").

  9. Close both files

  10. Delete temp file

paulsm4
  • 114,292
  • 17
  • 138
  • 190
  • Thanks for you answer, looks awesome too. I'm really appended to the idea of small files since it seams to be the way that have less amount of I/O because there will be just one append operation for each "middle insertion" proposed initially. Instead of all yours 10 steps I'll have just one append. Do you agree @paulsm4? – Frederico Schardong May 06 '12 at 04:37
0

I'm going to interpret your question broadly to be "how can I implement efficiently a persistent store of an object that supports random-access lookup by index and insertion with expansion." As noted, you could use a simple linear array in the file, but this would only be efficient for lookup (O(1)) and quite inefficient for insertion (O(n)). You could achieve O(log n) for both lookup and insertion by using a tree data structure instead. Maintain one file which acts as an index, and another which acts as the data store and is a series of chunks. Each chunk can be partially full. The index file contains a tree (binary tree or B-tree) where each node corresponds to some contiguous chunk of the array and contains the size of that chunk (so that the root node contains the size of the whole array). For a binary tree, the left and right child nodes contain the size of the left and right halves (approximately) of the array. Finally leaf nodes contain a pointer to a chunk in the data store file that contains the actual data. Insertion now involves changing the 'size' property of 'k' nodes, where 'k' is the height of the tree. When a data store chunk gets too full, split it (allocate a new one by growing the file, or, if you support deletion as well, perhaps from a free list of empty chunks) and rebalance the tree (lots of standard ways of doing this.)

Does this sound complicated? Definitely! Efficient mid-file insertion is more complicated to achieve than is appending.

gcbenison
  • 11,723
  • 4
  • 44
  • 82
  • As mentioned before my files can grow to 1GB, I'm expecting thousands of middle insertions. I was thinking about split the rouge file to small ones in which I'm able to append the new content in the end of them, then when all is done just join the small files. I believe that there is no solution more efficient than use small files through append operations. What do you think @gcbenison? – Frederico Schardong May 06 '12 at 04:25
  • You can keep a bunch of small files instead of small chunks in a big file, but you still need some way of indexing the chunks. If that way is to just stick a sequential label on each one, then insertion becomes O(n) because you have to update each one's label. Hence the tree-based approach. – gcbenison May 06 '12 at 04:29
  • Update each one's label? There is no need to do that. – Frederico Schardong May 06 '12 at 04:34
  • If a lot of the insertions land in the same chunk, at some point you'd need to split it, or you'll just end up inserting into the middle of a large file, which is what the chunked approach tries to avoid. And when you split the chunk, the indexing scheme needs to be updated so the new chunk can be found. – gcbenison May 06 '12 at 04:41
  • yes but anyway your tree data structure propose will have to perform a truncate for each new middle insert and consequently copy operations as proposed on other answers to my post, right? – Frederico Schardong May 06 '12 at 04:54