Skip to content

Fix Issue 18223: use memset in uninitializedFillDefault(T[])#6024

Merged
dlang-bot merged 1 commit intodlang:masterfrom
n8sh:uninitializedFillDefault-memset
Mar 31, 2018
Merged

Fix Issue 18223: use memset in uninitializedFillDefault(T[])#6024
dlang-bot merged 1 commit intodlang:masterfrom
n8sh:uninitializedFillDefault-memset

Conversation

@n8sh
Copy link
Member

@n8sh n8sh commented Jan 11, 2018

Current function in std.experimental.allocator.package:

private T[] uninitializedFillDefault(T)(T[] array) nothrow
{
    T t = T.init;
    fillWithMemcpy(array, t);
    return array;
}

When we can statically determine that the representation of T.init consists of nothing but zeroes we can instead use memset. char and wchar can also be special-cased.

@dlang-bot
Copy link
Contributor

dlang-bot commented Jan 11, 2018

Thanks for your pull request and interest in making D better, @n8sh! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please verify that your PR follows this checklist:

  • My PR is fully covered with tests (you can see the annotated coverage diff directly on GitHub with CodeCov's browser extension
  • My PR is as minimal as possible (smaller, focused PRs are easier to review than big ones)
  • I have provided a detailed rationale explaining my changes
  • New or modified functions have Ddoc comments (with Params: and Returns:)

Please see CONTRIBUTING.md for more information.


If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment.

Bugzilla references

Auto-close Bugzilla Severity Description
18223 enhancement std.experimental.allocator uninitializedFillDefault could use memset

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub fetch digger
dub run digger -- build "master + phobos#6024"

zero bits? Padding between a struct's fields is not considered.
+/
private template isAllZeroBits(T, T value)
{
Copy link
Member Author

Choose a reason for hiding this comment

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

Is there a more elegant way than this to check if T.init is entirely zero bits? typeid(T).initializer() is null isn't available.

foreach (i; 1 .. N)
buffer ~= " && isAllZeroBits!(E,value["~to!string(i)~"])";
return buffer;
}());
Copy link
Member Author

Choose a reason for hiding this comment

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

Is there a more elegant way to check all the elements of a static array?

~"), value."~fieldName~")";
}
return buffer;
}());
Copy link
Member Author

Choose a reason for hiding this comment

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

Is there a more elegant way to check all the fields of a struct?

@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch from 554a6f2 to bbf8757 Compare January 11, 2018 06:45
buffer ~= " && isAllZeroBits!(E,value["~to!string(i)~"])";
return buffer;
}());
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Is there a more elegant way to check all the elements of a static array?

Copy link
Contributor

Choose a reason for hiding this comment

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

Your version in "elegant":

enum isAllZeroBits = ()
{
    int b;
    static foreach (e; value)
        b += !isAllZeroBits!(typeof(e), e);

    return b == 0;
}();

Copy link
Contributor

Choose a reason for hiding this comment

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

And I think we can do even better here:

enum isAllZeroBits = isAllZeroBits!(typeof(value[0]), value[0]);

Static arrays need to have the same type, don't they?

Copy link
Member Author

Choose a reason for hiding this comment

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

That works only when the static array is the outermost type and not a field of another struct . That optimization could go in isInitAllZeroBits though.

@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch 3 times, most recently from 7f6c48c to 53c3865 Compare January 11, 2018 08:11
enum isAllZeroBits = mixin(()
{
enum N = typeof(value).length;
char[] buffer = new char[32 * value.length]; // Pre-size for value.length <= 999.
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't reserve the size. On append the slice is not the tail so it wil reallocate.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the correction! reserve isn't available at compile time so I'll just remove this.

@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch 3 times, most recently from ed54668 to 4cfb52e Compare January 11, 2018 23:39
@n8sh
Copy link
Member Author

n8sh commented Jan 11, 2018

Is there an escape hatch to bypass read-access protection during CTFE? Being unable to inspect private members of structs greatly reduces the applicability. Alternatively is there a better way to check at compile time if a struct's representation is entirely zero?

EDIT: Found the way using tupleof.

@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch 2 times, most recently from bdfe933 to 5a0ef22 Compare January 12, 2018 02:29
import core.stdc.string : memset;
if (array !is null)
memset(array.ptr, cast(ubyte) T.init, T.sizeof * array.length);
return array;
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't covered.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a unittest.

}());
}
else
enum isAllZeroBits = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

The style guide demands that once braces are used for an if statement that they are used for all cases.

char[] buffer;
buffer ~= "isAllZeroBits!(E,value[0])";
import std.conv : to;
foreach (i; 1 .. N)
Copy link
Contributor

Choose a reason for hiding this comment

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

could be static foreach

Copy link
Member Author

Choose a reason for hiding this comment

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

Because it could have been either I avoided it due to seeing this advice on the forums:

When you can use the old style do so. Since it puts less stress on the compiler in the general case.

Copy link
Contributor

@wilzbach wilzbach Jan 12, 2018

Choose a reason for hiding this comment

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

That's bad advice. There isn't any measurable performance penalty - I tried a large-scale test: #5989

enum isAllZeroBits = mixin(()
{
enum N = typeof(value).length;
char[] buffer;
Copy link
Contributor

Choose a reason for hiding this comment

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

could be string - you don't modify individual chars.

buffer ~= " && isAllZeroBits!(E,value["~to!string(i)~"])";
return buffer;
}());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Your version in "elegant":

enum isAllZeroBits = ()
{
    int b;
    static foreach (e; value)
        b += !isAllZeroBits!(typeof(e), e);

    return b == 0;
}();

if (buffer.length == 0)
buffer ~= "true";
return buffer;
}());
Copy link
Contributor

Choose a reason for hiding this comment

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

How about:

static if (value.tupleof.length == 0)
{
    enum isAllZeroBits = true;
}
else
{
    enum isAllZeroBits = ()
    {
        int b;
        static foreach (e; value.tupleof)
            b += !isAllZeroBits!(typeof(e), e);

        return b == 0;
    }();
}

buffer ~= " && isAllZeroBits!(E,value["~to!string(i)~"])";
return buffer;
}());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

And I think we can do even better here:

enum isAllZeroBits = isAllZeroBits!(typeof(value[0]), value[0]);

Static arrays need to have the same type, don't they?

@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch 4 times, most recently from 8038986 to 779cf53 Compare January 12, 2018 15:24
Copy link
Contributor

@wilzbach wilzbach left a comment

Choose a reason for hiding this comment

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

const(char)[] (aka string) is a typical use case, so I would add tests for arrays with const elements.

memset(array.ptr, 0, T.sizeof * array.length);
return array;
}
else static if (is(T : char) || (is(T : wchar) && T.init == 0xffff))
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. What's the reason for excluding dchar here?

  2. Shouldn't the type classifiers be removed? E.g.

  1. Also the T.init == 0xffff check is to support user-extended versions of (char) that don't overwrite the init value, right? This should probably get a test too.

Copy link
Member Author

@n8sh n8sh Jan 15, 2018

Choose a reason for hiding this comment

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

  1. dchar.sizeof == 4 && dchar.init == 0x0000_ffff. Can't use memset.

  2. static assert(is(immutable(char) : char)) compiles so I don't know that Unqual is necessary. I hadn't given much though to alias this. What do you suggest?

  3. Since char.sizeof == 1 memset will work regardless of what char.init is. Since wchar.sizeof == 2 memset will work if the high 8 bits are the same as the low 8 bits; the main purpose of the T.init == 0xffff check was reminding the casual reader that is is true for wchar. Maybe0 == (T.init & 0xff) ^ (T.init >>> 8) would be better?

// Has non-zero private fields.
import std.random : Mt19937;
static assert(!isInitAllZeroBits!Mt19937);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be worthwhile to check whether this works with const.
ConstOf might be helpful.

static assert(isInitAllZeroBits!P);
P[] a = [P(10, 11), P(20, 21), P(40, 41)];
uninitializedFillDefault(a);
assert(a == [P.init, P.init, P.init]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs tests for const, e.g

static struct S { float x = 0; float y = 0; }
static foreach (e; ["mutable", "non-mutable"])
{{

    static if (e == "mutable")
        alias P = S;
    else
        alias P = ConstOf!S;

    static assert(isInitAllZeroBits!P);
    P[] a = [P(10, 11), P(20, 21), P(40, 41)];
    uninitializedFillDefault(a);
    assert(a == [P.init, P.init, P.init]);
}}

fails with:

std/experimental/allocator/package.d(929): Error: function core.stdc.string.memset(return scope void* s, int c, ulong n) is not callable using argument types (const(S)*, int, ulong)
std/experimental/allocator/package.d(994): Error: template instance std.experimental.allocator.uninitializedFillDefault!(const(S)) error instantiating

Copy link
Member Author

@n8sh n8sh Jan 15, 2018

Choose a reason for hiding this comment

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

Shouldn't trying to write to const without casting const-ness away result in compilation failure?

EDIT (x2): When this function is used in makeArray const-ness is cast away by the caller. So I think not removing const-ness inside uninitializedFillDefault is consistent with the current design.

alias U = Unqual!T;
return () @trusted { return cast(T[]) uninitializedFillDefault(cast(U[]) m); }();

package template isInitAllZeroBits(T)
{
import std.traits : isStaticArray;
static if (isStaticArray!T)
Copy link
Member

Choose a reason for hiding this comment

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

I would use a more generic trait: static if (T.sizeof == 0).

@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch 2 times, most recently from 5f9a7c6 to 6fb050c Compare January 15, 2018 17:12
@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch 3 times, most recently from a072bf8 to fc69535 Compare February 12, 2018 18:43
@apz28
Copy link

apz28 commented Feb 13, 2018

Improvement codes, loop should not be continue if isAllZeroBits return false.

  •        static foreach (i; 0 .. T.length)
    
  •            b &= isAllZeroBits!(typeof(value[i]), value[i]);
    

@n8sh
Copy link
Member Author

n8sh commented Feb 15, 2018

@apz28 The problem is because I am using static foreach if I return before the end the compiler will complain about unreachable code.

@n8sh
Copy link
Member Author

n8sh commented Feb 16, 2018

@apz28 I've fixed the issue you raised.

@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch from fc69535 to 192220f Compare February 16, 2018 00:25
@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch 6 times, most recently from 6296021 to b8c2b01 Compare March 14, 2018 21:01
@n8sh
Copy link
Member Author

n8sh commented Mar 14, 2018

Copy link
Contributor

@wilzbach wilzbach left a comment

Choose a reason for hiding this comment

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

Noice!

@wilzbach wilzbach added the Merge:72h no objection -> merge The PR will be merged if there are no objections raised. label Mar 28, 2018
@wilzbach wilzbach added this to the 2.080.0 milestone Mar 28, 2018
@wilzbach
Copy link
Contributor

Putting this on the merge queue (if no objections are raised in the next three days, this can be merged).

@n8sh n8sh dismissed DmitryOlshansky’s stale review March 28, 2018 01:47

Applies to code that was removed.

@wilzbach
Copy link
Contributor

BTW @n8sh we have this semi-official Slack channel that we often for some quick discussion. I couldn't find your email address, so if you want to join, just send me an email ;-)

static assert(isAllOneBits!(char, 0xff));
static assert(isAllOneBits!(wchar, 0xffff));
static assert(isAllOneBits!(byte, cast(byte) 0xff));
static assert(isAllOneBits!(int, 0xffff_ffff));
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer to see tests for all integer types here just to be sure.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added.

@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch 2 times, most recently from d52186b to aa8f45a Compare March 28, 2018 17:20
When we can statically determine that the representation of T.init
consists of nothing but zeroes or nothing but ones we use memset.
(The second case occurs for char and wchar.)
@n8sh n8sh force-pushed the uninitializedFillDefault-memset branch from aa8f45a to 8777cbf Compare March 28, 2018 17:59
@n8sh n8sh added Merge:auto-merge and removed Review:Needs Review Merge:72h no objection -> merge The PR will be merged if there are no objections raised. labels Mar 31, 2018
@dlang-bot dlang-bot merged commit 6797b52 into dlang:master Mar 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants