Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java/C#/Python prefixed size support #4445

Merged
merged 44 commits into from
Mar 12, 2018

Conversation

robert-schmidtke
Copy link
Contributor

@robert-schmidtke robert-schmidtke commented Oct 2, 2017

Hi,

this is a WIP PR that hopefully eventually adds the possibility to prefix the buffer's size to it, just like in C++ with FinishSizePrefixed and GetSizePrefixedRoot. I have successfully tested the Java part in a separate project of mine, however the C# variant is untested. I would therefore like to ask your advice on how to best add unit tests for the changes, and how to run them on either a Linux or macOS box.

In the getSizePrefixedRoot code I generate, I always assume the length is 4 bytes. Having read the documentation, this is intentional, right? Otherwise there would need to be some check (but since ByteBuffer.capacity() is an int that will not be necessary, I think).

In general I'm of course happy for any feedback, and whether my understanding of the FlatBufferBuilders is correct, and whether my approach of implementation is compatible with the C++ version (a unit test one could add).

The build is succeeding: https://travis-ci.org/robert-schmidtke/flatbuffers/builds/282171784 Having not looked too much into what make test actually does, this may not cover all cases. I'll be working simultaneously on adding tests.

Cheers
Robert

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed, please reply here (e.g. I signed it!) and we'll verify. Thanks.


  • If you've already signed a CLA, it's possible we don't have your GitHub username or you're using a different email address. Check your existing CLA data and verify that your email is set on your git commits.
  • If your company signed a CLA, they designated a Point of Contact who decides which employees are authorized to participate. You may need to contact the Point of Contact for your company and ask to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the project maintainer to go/cla#troubleshoot.
  • In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again.

@robert-schmidtke
Copy link
Contributor Author

I signed it!

@googlebot
Copy link

CLAs look good, thanks!

@robert-schmidtke
Copy link
Contributor Author

Both the Java and C# tests succeed as well now, the Java part of which also tests the C++ written, binary size prefixed monster file.

@aardappel
Copy link
Collaborator

You can test under Linux with Mono, see tests/FlatBuffers.Test/NetTest.sh.

You may assume size 4, yes. It be nice to use a constant, though, either and existing or new one in Constants.java and FlatBufferConstants.cs

code += ps_method_signature + "(ByteBuffer _psbb, " + struct_def.name + " obj) { ";
code += "ByteBuffer _bb = _psbb." + FunctionStart('S') + "lice(); ";
if (lang_.language == IDLOptions::kCSharp) {
code += "_bb.Position = 4; ";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a constant for Java and C# each. The Java one had to be explicitly public though, because the other ones are package-private.

@@ -828,6 +828,33 @@ void GenStruct(StructDef &struct_def, std::string *code_ptr) {
code += ") + _bb.";
code += lang_.get_bb_position;
code += ", _bb)); }\n";

// recreate both methods for the prefixed size version
std::string ps_method_name = FunctionStart('G') + "etSizePrefixedRootAs" +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has a lot of overlap with the code above, any ideas to refactor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see something like I've done in lines 1335+. However the method bodies would depend on i, sort of forcing the for-switch anti-pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a version with less overlap in e84452f.

file_identifier_.length() ? file_identifier_.c_str() : nullptr);
if (opts.prefix_size) {
builder_.FinishSizePrefixed(Offset<Table>(toff),
file_identifier_.length() ? file_identifier_.c_str() : nullptr);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe at least pull the file identifier arg out of the if?

@@ -14,6 +14,9 @@
public final class Monster extends Table {
public static Monster getRootAsMonster(ByteBuffer _bb) { return getRootAsMonster(_bb, new Monster()); }
public static Monster getRootAsMonster(ByteBuffer _bb, Monster obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
public static Monster getSizePrefixedRootAsMonster(ByteBuffer _psbb) { return getSizePrefixedRootAsMonster(_psbb, new Monster()); }
public static Monster getSizePrefixedRootAsMonster(ByteBuffer _psbb, Monster obj) { ByteBuffer _bb = _psbb.slice(); _bb.position(4); return getRootAsMonster(_bb, obj); }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we're creating a new ByteBuffer here, ideally this refers to the existing one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a new buffer created, however slice() uses the data of the other buffer, there is no copying involved, only position, limit etc. are reset. The same happens with the Slice() version I have added to the C# ByteBuffer. My reasoning behind this was that I wanted to avoid advancing the passed-in _psbb to avoid side-effects for the user. One possibility would be to increment the position by 4, do the regular getRootAsMonster, and then decrement it again. However I was worried about concurrent access to the buffer. Any thoughts on this? Would the +4, getRootAsMonster, -4 version be okay with you?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware we're not copying the buffer, but I generally like to avoid object allocations where possible.
I guess the + then - trick would be ok, but really we shouldn't need to touch the position at all if you see how the existing getRootAsMonster works, you can just generate a similar method that does _bb.position() + 4 on the fly.

@@ -0,0 +1,10 @@
// similar schema, except to be parsed with --prefix-size

include "monster_test.fbs";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer it if we keep the test simple without needing yet another schema. You could simply generate a size-prefixed buffer in code, and then see if you can access it correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that, I have thought about it similarly. However, I'm not quite sure how else I may check the interoperability between C++ and Java (in this case), if not for a generated file. One could add another dedicated test that checks for interoperability in all supported languages? This test would first generate the regular and the size-prefixed version, and then invoke a small test for each language. This way, the new schema would not need to be versioned in git.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that an intra-language test is that important, since all we'd be checking is if a language uses the right size (32 bits)..

@robert-schmidtke
Copy link
Contributor Author

I have removed the explicit tests for the size prefixed version, and added inline tests. This brought to my attention, that it might be useful to introduce SizePrefixedMonsterBufferHasIdentifier methods that do not rely on the fact, that the user has advanced the provided buffer by 4 bytes themselves?

@aardappel
Copy link
Collaborator

I was thinking, that rather than introducing all these new generated methods (which few people will use, and may confuse), why don't we simply have non-generated functions that can either advance a size prefixed ByteBuffer, or slice into a new ByteBuffer? There is nothing type specific about these functions. You'd then simply call getRootAsMonster(bb.FromSizePrefixed()) or whatever.

@robert-schmidtke
Copy link
Contributor Author

I understand. Let me try and rework the additional code into a subclass of ByteBuffer that capsules the slicing behavior, essentially leaving the original ByteBuffer alone.

@robert-schmidtke
Copy link
Contributor Author

So I managed to encapsulate the slicing behavior in a subclass of ByteBuffer, such that 'pure' ByteBuffers do not suffer from the overhead of carrying the _off. Is that more acceptable for you?


public byte[] Data { get { return _buffer; } }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is Data removed? was this unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data was used, mainly for length checks and memory copies. I thought that fully hiding the underlying buffer was slightly better design, especially when dealing with slices. Thus, the buffer's content is modified/read using the Put* and Get* methods.

_buffer = newBuffer;
}

public virtual byte[] ToArray(int pos, int len)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some methods here are virtual and others not.. any reason? I don't think we intend this class to be inherited from, certainly not if it affects performance for basic functions like GetInt etc, which it may.

return ToArray(Position, Length - Position);
}

public byte[] ToFullArray()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the use of this one? Why would anyone ever want the unused part of the buffer copied?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mainly for testing to check that the padding is correct. This was previously done by checking against Data directly.


// Increases the size of the ByteBuffer, and copies the old data towards
// the end of the new buffer.
public virtual void GrowFront(int newSize)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did this function come from / where is it called from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By hiding Data, I moved growing the buffer from FlatBufferBuilder to ByteBuffer. This is a 1:1 copy of that method. It's called from FlatBufferBuilder::GrowBuffer.

return base.GetUshort(_off + index);
}

public override int GetInt(int index)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is called a LOT, so it potentially becoming slower by being virtual I am afraid is not acceptable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I frankly do not see another way to support slices, which imho is the cleanest way of representing size-prefixed buffers. Note that using ByteBuffer in Java has exactly the same design, even for non-slices. The last resort would be do create a new ByteBuffer with a separate underlying byte[] array and copy all the previous contents to it, minus the size prefix. That would be a one-time overhead for creating the slice, and about 100% memory overhead, unless the caller discards all references to the size-prefixed buffer, and lets the GC handle that.

Copy link
Contributor Author

@robert-schmidtke robert-schmidtke Mar 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI, I have added a small benchmark:

[FlatBuffersTestMethod]
public void BenchmarkFlatBuffer()
{
    Random rnd = new Random(0);
    ByteBuffer bb = new ByteBuffer(File.ReadAllBytes(@"Resources/monsterdata_test.mon"));
    // bb = bb.Slice(); // toggle for ByteBufferSlice performance
    int maxIndex = bb.Length - 4;
    int sum = 0;

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 1000000; ++i)
    {
        sum += bb.GetInt(rnd.Next(maxIndex));
    }
    sw.Stop();

    // write sum to avoid loop optimization
    Console.WriteLine("Regular: {0}, {1} GetInt ops/ms", sum, 1000000.0 / sw.Elapsed.TotalMilliseconds);
}

I ran it on my Mac ten times with the regular ByteBuffer and with the ByteBufferSlice and averaged the results. Numbers are GetInt operations per millisecond.

Version Safe Unsafe
original ByteBuffer (current master) 33,876 44,436
ByteBuffer using + _off 33,790 (-0.3%) 43,837 (-1.3%)
ByteBuffer using virtual 33,017 (-2.5%) 44,017 (-1.0%)
ByteBufferSlice using override 32,400 (-4.4%) 42,877 (-3.5%)

This is an extremely dense testing scenario, and should thus represent the worst case performance degradation.

Given these results I'd vote for the slicing behavior implementation from 5c68259, as I think a 0.3% to 1.0% performance decrease would be acceptable.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You say this is on your Mac.. does that mean these timings are from Mono, or the MS implementation?

If this is using the MS JIT, then indeed you have a good point that performance is acceptable for _off. Though part of this may be that the C# ByteBuffer is already quite slow.

I think we can do better though. If you look at how getting the root works in C# generated code:

public static Monster GetRootAsMonster(ByteBuffer _bb, Monster obj) { return (obj.__assign(_bb.GetInt(_bb.Position) + _bb.Position, _bb)); }

This tells me, that to create a root from a size-prefixed buffer, all we need to do is pass it a ByteBuffer whose _pos has been moved forward by 4 bytes. All buffer accesses from there on are absolute, using the bb_pos stored in these table accessor structs as the starting point, so no need to keep adding _off at each access. And likely more robust since we can't forget to add _off somewhere.

Am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, the timings are from Mono:

Mono JIT compiler version 5.4.1.7 (2017-06/e66d9abbb27 Wed Oct 25 12:10:41 EDT 2017)
Copyright (C) 2002-2014 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com

Compiled with mcs:

Mono C# compiler version 5.4.1.0

I'm not sure there is an alternative on Mac? Should I get my hands on a Windows machine and benchmark it there as well?

I was thinking to separate _pos and _off, because _pos can be set externally, and therefore the size prefix could be restored (accidentally, e.g. by Reset()). For the use case where bb_pos is used as offset (when interacting with the ByteBuffer through Monster) you're correct, but since the ByteBuffer may be used rather freely I figured it'd be better to logically separate them.

I have added your suggestion, and these are the results:

Version Safe Unsafe
original ByteBuffer (current master) 33,876 44,436
ByteBuffer using + _off 33,790 (-0.3%) 43,837 (-1.3%)
ByteBuffer using virtual 33,017 (-2.5%) 44,017 (-1.0%)
ByteBufferSlice using override 32,400 (-4.4%) 42,877 (-3.5%)
ByteBuffer using advanced _pos 32,263 (-4.8%) 45,593 (+2.6%)

I don't really know why this performs so poorly, I guess it's simply not the exact same system load today than it was last time. Your approach would certainly save a lot of code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supposedly parts of MS's implementation are portable now and run on Linux, and I presume OS X as well, but haven't looked into it. It is entirely possible that Mono is not quite as optimized.

I'd prefer it if we use just a single variable, for efficency (possibly), especially since size-prefixing is such a minor feature compared to the base functionality. To screw this up, someone would have to load a size-prefixed buffer, slice it to obtain a new one, then call Reset on it, and somehow not understand that will get you your size prefixing back? That sounds unlikely to me.

I'm not sure if we're getting the correct results here. You're only calling bb.GetInt which doesn't even touch _pos! It directly uses its argument into _buffer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GetInt in the ByteBufferSlice subclass would add _off in the call to base.GetInt, whereas the GetInt in ByteBuffer does not, correct. Hence the regular vs. Slice comparison.

However the current state implements your suggestion of simply passing an advanced ByteBuffer to GetRootAsMonster. For clarity I changed the terminology from 'slice' to 'duplicate', also for the Java version.

@aardappel
Copy link
Collaborator

Otherwise looks good to me now!

@robert-schmidtke
Copy link
Contributor Author

Should I squash the commits into one, or will you do that upon merging eventually?

@aardappel
Copy link
Collaborator

No, I'll squash it on merge.
Just waiting for CI. I presume you are "done" with this PR?

@robert-schmidtke
Copy link
Contributor Author

Unless you have any more comments/requested changes, I don't think I have anything to add.

@aardappel aardappel merged commit 08cf50c into google:master Mar 12, 2018
@aardappel
Copy link
Collaborator

Thanks for sticking with it :)

@robert-schmidtke robert-schmidtke deleted the java-prefixed-size branch March 13, 2018 08:02
@iceboy233 iceboy233 mentioned this pull request May 1, 2018
zchee pushed a commit to zchee/flatbuffers that referenced this pull request Feb 14, 2019
* initial changes to support size prefixed buffers in Java

* add slice equivalent to CSharp ByteBuffer

* resolve TODO for slicing in CSharp code generation

* add newly generated Java and CSharp test sources

* fix typo in comment

* add FinishSizePrefixed methods to CSharp FlatBufferBuilder as well

* add option to allow writing the prefix as well

* generate size-prefixed monster binary as well

* extend JavaTest to test the size prefixed binary as well

* use constants for size prefix length

* fuse common code for getRootAs and getSizePrefixedRootAs

* pulled file identifier out of if

* add FinishSizePrefixed, GetSizePrefixedRootAs support for Python

* Revert "extend JavaTest to test the size prefixed binary as well"

This reverts commit 68be442.

* Revert "generate size-prefixed monster binary as well"

This reverts commit 2939516.

* fix ByteBuffer.cs Slice() method; add proper CSharp and Java tests

* fix unused parameter

* increment version number

* pulled out generated methods into separate utility class

* pulled out generated methods into separate utility class for Python

* fix indentation

* remove unnecessary comment

* fix newline and copyright

* add ByteBufferUtil to csproj compilation

* hide ByteBuffer's internal data; track offset into parent's array

* test unsafe versions as well; compile and run in debug mode

* clarify help text for size prefix

* move ByteBuffer slicing behavior to subclass

* fix protection levels

* add size prefix support for text generation

* add ByteBufferSlice to csproj compilation

* revert size prefix handling for nested buffers

* use duplicate instead of slice for removing size prefix

* remove slice subclass and use duplicate for removing size prefix

* remove slice specific tests

* remove superfluous command line option
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants