たとえば、int
をlong
にアップグレードする代わりに、32ビット整数がオーバーフローしている場合、2以内の範囲のみが必要な場合、40ビット型を使用できますか40、すべての整数に対して24(64-40)ビットを節約しますか?
もしそうなら、どのように?
私は数十億に対処しなければならず、スペースはより大きな制約です。
確かに可能ですが、通常は無意味です(これらの数のbillionsを使用しないプログラムの場合):
#include <stdint.h> // don't want to rely on something like long long
struct bad_idea
{
uint64_t var : 40;
};
ここで、var
は実際には40ビットの幅を持ちますが、 ずっと 効率の悪いコードが生成され(「多く」が非常に間違っていることがわかりました-測定されたオーバーヘッドは1〜2%にすぎません。以下のタイミングを参照)、通常は役に立ちません。同じ構造にパックしたい別の24ビット値(または8ビットと16ビットの値)が必要でない限り、アラインメントは得られるものをすべて失います。
いずれにせよ、これらの数十億を持たない限り、メモリ消費の効果的な違いは目立たないでしょう(しかし、ビットフィールドを管理するために必要な余分なコードは目立ちます!)。
注:
質問は、実際にはbillionsの数字が必要であることを反映するように更新されました。したがって、これは実行可能なことかもしれません。残りの24ビットに何かを保存するか、各8またはその倍数の構造に40ビット値を保存することにより、構造のアライメントとパディングによってゲインを失わない方法。
3バイトを10億回節約する価値があります。必要なメモリページが大幅に少なくなり、キャッシュとTLBミスが少なくなり、とりわけページフォールト(単一ページフォールトの重み付け数千万命令)。
上記のスニペットは残りの24ビットを使用しませんが(「40ビットを使用する」部分を示すだけです)、メモリを保存するという意味でアプローチを実際に有効にするには、次のようなものが必要です。あなたは確かに穴に入れる他の「有用な」データを持っています:
struct using_gaps
{
uint64_t var : 40;
uint64_t useful_uint16 : 16;
uint64_t char_or_bool : 8;
};
構造体のサイズと配置は64ビット整数に等しいので、たとえば10億のそのような構造の配列(コンパイラー固有の拡張機能を使用しなくても)。 8ビット値を使用しない場合は、48ビット値と16ビット値を使用することもできます(オーバーフローマージンが大きくなります)。
また、使いやすさを犠牲にして、8つの40ビット値を構造に入れることもできます(40と64の最小公倍数は320 = 8 * 40です)。もちろん、構造体の配列の要素にアクセスするコードはmuchより複雑になります(おそらくoperator[]
線形配列機能を復元し、構造の複雑さを隠します。
更新:
ビットフィールド(およびビットフィールドrefによる演算子のオーバーロード)のオーバーヘッドを確認するために、クイックテストスイートを作成しました。 gcc.godbolt.org に投稿されたコード(長さによる)、Win7-64マシンからのテスト出力は次のとおりです。
Running test for array size = 1048576
what alloc seq(w) seq(r) Rand(w) Rand(r) free
-----------------------------------------------------------
uint32_t 0 2 1 35 35 1
uint64_t 0 3 3 35 35 1
bad40_t 0 5 3 35 35 1
packed40_t 0 7 4 48 49 1
Running test for array size = 16777216
what alloc seq(w) seq(r) Rand(w) Rand(r) free
-----------------------------------------------------------
uint32_t 0 38 14 560 555 8
uint64_t 0 81 22 565 554 17
bad40_t 0 85 25 565 561 16
packed40_t 0 151 75 765 774 16
Running test for array size = 134217728
what alloc seq(w) seq(r) Rand(w) Rand(r) free
-----------------------------------------------------------
uint32_t 0 312 100 4480 4441 65
uint64_t 0 648 172 4482 4490 130
bad40_t 0 682 193 4573 4492 130
packed40_t 0 1164 552 6181 6176 130
見ることができるのは、ビットフィールドの余分なオーバーヘッドは無視できることですが、キャッシュフレンドリーな方法で直線的にデータにアクセスする場合、演算子がビットフィールド参照を便利なものとしてオーバーロードするのはかなり劇的です(約3倍増加)。一方、ランダムアクセスでは、ほとんど問題になりません。
これらのタイミングは、64ビット整数を使用するほうが、ビットフィールドよりも全体的に高速であるため(より多くのメモリに触れるにもかかわらず)優れていることを示唆していますが、もちろん、より大きなデータセットでのページフォールトのコストを考慮していません。物理的なRAM(私はそれをテストしなかった)を使い果たすと、それは非常に異なって見えるかもしれません。
次のように、4 * 40ビットの整数を非常に効果的に160ビットの構造体にパックできます。
struct Val4 {
char hi[4];
unsigned int low[4];
}
long getLong( const Val4 &pack, int ix ) {
int hi= pack.hi[ix]; // preserve sign into 32 bit
return long( (((unsigned long)hi) << 32) + (unsigned long)pack.low[i]);
}
void setLong( Val4 &pack, int ix, long val ) {
pack.low[ix]= (unsigned)val;
pack.hi[ix]= (char)(val>>32);
}
これらは再び次のように使用できます。
Val4[SIZE] vals;
long getLong( int ix ) {
return getLong( vals[ix>>2], ix&0x3 )
}
void setLong( int ix, long val ) {
setLong( vals[ix>>2], ix&0x3, val )
}
おそらく、これらの番号の多くをどこかに(RAM、ディスク、ネットワーク経由で送信するなど)格納し、それらを1つずつ取り出して処理を行うでしょう。
1つのアプローチは、VLEを使用してencodeすることです。 Googleのprotobufから ドキュメント (CreativeCommonsライセンス)
ヴァリントは、1つ以上のバイトを使用して整数をシリアル化する方法です。数値が小さいほど、バイト数は少なくなります。
最後のバイトを除く、varintの各バイトには最上位ビット(msb)が設定されています。これは、さらにバイトが来ることを示します。各バイトの下位7ビットは、7ビットのグループで、最下位グループを最初に、数値の2の補数表現を格納するために使用されます。
したがって、たとえば、ここに数字1があります。これは1バイトなので、msbは設定されていません。
0000 0001
そして、これは300です–これはもう少し複雑です:
1010 1100 0000 0010
これが300であることをどのように把握しますか?まず、各バイトからmsbをドロップします。これは、数字の最後に到達したかどうかを確認するためのものです(ご覧のとおり、varintには複数のバイトがあるため、最初のバイトに設定されています)
長所
短所
(編集:まず第一に-あなたが望むものは可能であり、いくつかのケースでは理にかなっています.Netflixチャレンジのために何かをしようとして、1GBのメモリしか持っていなかったとき、私は同様のことをしなければなりませんでした。第二-それはおそらく最高です40ビットストレージにchar配列を使用して、アライメントの問題や構造体パッキングプラグマを台無しにする必要性を回避するために、3番目-この設計では、中間結果に対して64ビット演算で問題ないと想定しています。 Int40を使用する配列ストレージ;第4:これが悪い考えであるという提案はすべて得られません。メッシュデータ構造をパックするために人々が経験することを読んでください。これは比較すると子供の遊びのように見えます)。
必要なのは、データを40ビットintとして保存するためだけに使用される構造体ですが、算術演算のために暗黙的にint64_tに変換します。唯一のトリックは、40ビットから64ビットまでの符号拡張を正しく行うことです。符号なし整数で問題なければ、コードはさらに簡単になります。これで開始できます。
#include <cstdint>
#include <iostream>
// Only intended for storage, automatically promotes to 64-bit for evaluation
struct Int40
{
Int40(int64_t x) { set(static_cast<uint64_t>(x)); } // implicit constructor
operator int64_t() const { return get(); } // implicit conversion to 64-bit
private:
void set(uint64_t x)
{
setb<0>(x); setb<1>(x); setb<2>(x); setb<3>(x); setb<4>(x);
};
int64_t get() const
{
return static_cast<int64_t>(getb<0>() | getb<1>() | getb<2>() | getb<3>() | getb<4>() | signx());
};
uint64_t signx() const
{
return (data[4] >> 7) * (uint64_t(((1 << 25) - 1)) << 39);
};
template <int idx> uint64_t getb() const
{
return static_cast<uint64_t>(data[idx]) << (8 * idx);
}
template <int idx> void setb(uint64_t x)
{
data[idx] = (x >> (8 * idx)) & 0xFF;
}
unsigned char data[5];
};
int main()
{
Int40 a = -1;
Int40 b = -2;
Int40 c = 1 << 16;
std::cout << "sizeof(Int40) = " << sizeof(Int40) << std::endl;
std::cout << a << "+" << b << "=" << (a+b) << std::endl;
std::cout << c << "*" << c << "=" << (c*c) << std::endl;
}
以下に、実際に試すためのリンクを示します。 http://rextester.com/QWKQU25252
ビットフィールド構造を使用できますが、メモリを節約することはできません。
struct my_struct
{
unsigned long long a : 40;
unsigned long long b : 24;
};
このような8つの40ビット変数の倍数を1つの構造に圧縮できます。
struct bits_16_16_8
{
unsigned short x : 16;
unsigned short y : 16;
unsigned short z : 8;
};
struct bits_8_16_16
{
unsigned short x : 8;
unsigned short y : 16;
unsigned short z : 16;
};
struct my_struct
{
struct bits_16_16_8 a1;
struct bits_8_16_16 a2;
struct bits_16_16_8 a3;
struct bits_8_16_16 a4;
struct bits_16_16_8 a5;
struct bits_8_16_16 a6;
struct bits_16_16_8 a7;
struct bits_8_16_16 a8;
};
これにより、メモリを節約できます(8つの「標準」64ビット変数を使用する場合と比較して)が、これらの変数のすべての操作(特に算術操作)をいくつかの操作に分割する必要があります。
そのため、メモリの最適化は、ランタイムパフォーマンスと「トレード」されます。
コメントが示唆するように、これはかなりの作業です。
おそらく不必要な手間nless RAM-の多くを保存したいなら、それはもっと理にかなっています。(RAMの保存は、数百万の保存されたビットの合計long
値はRAMに保存されます)
5バイト/文字(5 * 8ビット= 40ビット)の配列の使用を検討します。次に、(オーバーフローしたint-したがってlong
)値からバイト配列にビットをシフトして、それらを格納する必要があります。
値を使用するには、ビットをシフトしてlong
に戻します。値を使用できます。
次に、RAMと値のファイルストレージは40ビット(5バイト)になりますが、struct
を使用して5バイトを保持する場合は、データのアライメントを考慮する必要があります。このビットシフトとデータアライメントへの影響について詳しく説明する必要がある場合はお知らせください。
同様に、64ビットのlong
を使用し、使用したくない残りの24ビットでhideその他の値(おそらく3文字)を使用できます。再び-ビットシフトを使用して24ビット値を追加および削除します。
私はそれを仮定します
unsigned char hugearray[5*size+3]; // +3 avoids overfetch of last element
__int64 get_huge(unsigned index)
{
__int64 t;
t = *(__int64 *)(&hugearray[index*5]);
if (t & 0x0000008000000000LL)
t |= 0xffffff0000000000LL;
else
t &= 0x000000ffffffffffLL;
return t;
}
void set_huge(unsigned index, __int64 value)
{
unsigned char *p = &hugearray[index*5];
*(long *)p = value;
p[4] = (value >> 32);
}
2つのシフトでgetを処理する方が速い場合があります。
__int64 get_huge(unsigned index)
{
return (((*(__int64 *)(&hugearray[index*5])) << 24) >> 24);
}
役に立つかもしれない別のバリエーションは、構造を使用することです:
typedef struct TRIPLE_40 {
uint32_t low[3];
uint8_t hi[3];
uint8_t padding;
};
このような構造には16バイトが必要で、16バイトにアラインすると、完全に1つのキャッシュラインに収まります。構造のどの部分を使用するかを特定すると、構造が3つではなく4つの要素を保持する場合よりもコストが高くなる可能性がありますが、1つのキャッシュラインへのアクセスは2つへのアクセスよりもはるかに安価です。パフォーマンスが重要な場合、一部のマシンはdivmod-3操作を安価に実行し、キャッシュラインフェッチあたりのコストが高いため、他のマシンはメモリアクセスが安く、高価なdivmod-3があるため、ベンチマークを使用する必要があります。
数十億の40ビット符号付き整数を格納し、8ビットのバイトを想定する場合、8つの40ビット符号付き整数を構造体にパックできます(以下のコードでは、バイト配列を使用してこれを行います)。この構造体は通常整列されているため、そのようなパックされたグループの論理配列を作成し、その通常の順次インデックスを提供できます。
#include <limits.h> // CHAR_BIT
#include <stdint.h> // int64_t
#include <stdlib.h> // div, div_t, ptrdiff_t
#include <vector> // std::vector
#define STATIC_ASSERT( e ) static_assert( e, #e )
namespace cppx {
using Byte = unsigned char;
using Index = ptrdiff_t;
using Size = Index;
// For non-negative values:
auto roundup_div( const int64_t a, const int64_t b )
-> int64_t
{ return (a + b - 1)/b; }
} // namespace cppx
namespace int40 {
using cppx::Byte;
using cppx::Index;
using cppx::Size;
using cppx::roundup_div;
using std::vector;
STATIC_ASSERT( CHAR_BIT == 8 );
STATIC_ASSERT( sizeof( int64_t ) == 8 );
const int bits_per_value = 40;
const int bytes_per_value = bits_per_value/8;
struct Packed_values
{
enum{ n = sizeof( int64_t ) };
Byte bytes[n*bytes_per_value];
auto value( const int i ) const
-> int64_t
{
int64_t result = 0;
for( int j = bytes_per_value - 1; j >= 0; --j )
{
result = (result << 8) | bytes[i*bytes_per_value + j];
}
const int64_t first_negative = int64_t( 1 ) << (bits_per_value - 1);
if( result >= first_negative )
{
result = (int64_t( -1 ) << bits_per_value) | result;
}
return result;
}
void set_value( const int i, int64_t value )
{
for( int j = 0; j < bytes_per_value; ++j )
{
bytes[i*bytes_per_value + j] = value & 0xFF;
value >>= 8;
}
}
};
STATIC_ASSERT( sizeof( Packed_values ) == bytes_per_value*Packed_values::n );
class Packed_vector
{
private:
Size size_;
vector<Packed_values> data_;
public:
auto size() const -> Size { return size_; }
auto value( const Index i ) const
-> int64_t
{
const auto where = div( i, Packed_values::n );
return data_[where.quot].value( where.rem );
}
void set_value( const Index i, const int64_t value )
{
const auto where = div( i, Packed_values::n );
data_[where.quot].set_value( where.rem, value );
}
Packed_vector( const Size size )
: size_( size )
, data_( roundup_div( size, Packed_values::n ) )
{}
};
} // namespace int40
#include <iostream>
auto main() -> int
{
using namespace std;
cout << "Size of struct is " << sizeof( int40::Packed_values ) << endl;
int40::Packed_vector values( 25 );
for( int i = 0; i < values.size(); ++i )
{
values.set_value( i, i - 10 );
}
for( int i = 0; i < values.size(); ++i )
{
cout << values.value( i ) << " ";
}
cout << endl;
}
数十億の整数を処理する必要がある場合は、single40の代わりに40ビット数のarraysをカプセル化しようとしますビット数。そうすれば、コードの残りの部分を変更せずに、異なる配列実装(たとえば、データをオンザフライで圧縮する実装、または使用頻度の低いデータをディスクに保存する実装)をテストできます。
以下に実装例を示します( http://rextester.com/SVITH57679 ):
class Int64Array
{
char* buffer;
public:
static const int BYTE_PER_ITEM = 5;
Int64Array(size_t s)
{
buffer=(char*)malloc(s*BYTE_PER_ITEM);
}
~Int64Array()
{
free(buffer);
}
class Item
{
char* dataPtr;
public:
Item(char* dataPtr) : dataPtr(dataPtr){}
inline operator int64_t()
{
int64_t value=0;
memcpy(&value, dataPtr, BYTE_PER_ITEM); // Assumes little endian byte order!
return value;
}
inline Item& operator = (int64_t value)
{
memcpy(dataPtr, &value, BYTE_PER_ITEM); // Assumes little endian byte order!
return *this;
}
};
inline Item operator[](size_t index)
{
return Item(buffer+index*BYTE_PER_ITEM);
}
};
注:40ビットから64ビットへのmemcpy
変換は、リッテエンディアンを前提としているため、基本的に未定義の動作です。ただし、x86プラットフォームで動作するはずです。
注2:明らかに、これは概念実証用のコードであり、本番用のコードではありません。実際のプロジェクトで使用するには、(特に)追加する必要があります。
Item
には、値セマンティクスではなく参照セマンティクスがありますが、これはoperator[]
;おそらく、いくつかの巧妙なC++型変換のトリックでそれを回避できます。これらはすべてC++プログラマーにとって簡単なはずですが、明確にすることなくサンプルコードをはるかに長くするため、それらを省略することにしました。
はい、あなたはそれを行うことができ、それは大量の数字のためにいくつかのスペースを節約します
符号なし整数型のstd :: vectorを含むクラスが必要です。
整数を保存および取得するには、メンバー関数が必要です。たとえば、それぞれ40ビットの64個の整数を保存する場合は、それぞれ64ビットの40個の整数のベクトルを使用します。次に、インデックスが[0,64]の整数を格納するメソッドと、そのような整数を取得するメソッドが必要です。
これらのメソッドは、いくつかのシフト操作、およびいくつかのバイナリを実行します。および&.
あなたの質問はあまり具体的ではないので、ここでは詳細をまだ追加していません。整数をいくつ保存したいか知っていますか?コンパイル時に知っていますか?プログラムの開始時に知っていますか?整数はどのように編成する必要がありますか?配列が好きですか?地図が好きですか?整数をより少ないストレージに詰め込もうとする前に、これらすべてを知っておく必要があります。
ここには実装に関する回答がかなりありますので、アーキテクチャについてお話したいと思います。
アーキテクチャは64ビット値を処理するように設計されているため、オーバーフローを避けるために通常32ビット値を64ビット値に拡張します。
ほとんどのアーキテクチャは、サイズが2の累乗である整数で動作するように設計されています。これにより、ハードウェアが大幅に簡素化されるためです。キャッシングなどのタスクは、このようにはるかに簡単です。2の累乗に固執する場合、ビットマスキングとシフトに置き換えることができる除算とモジュラス演算が多数あります。
これがどれほど重要かを示す例として、C++ 11仕様では、「メモリロケーション」に基づいてマルチスレッドレースケースを定義しています。メモリロケーションは1.7.3で定義されています。
メモリ位置は、スカラー型のオブジェクトか、すべてがゼロ以外の幅を持つ隣接するビットフィールドの最大シーケンスのいずれかです。
つまり、C++のビットフィールドを使用する場合は、すべてのマルチスレッドを慎重に行う必要があります。隣接する2つのビットフィールドは、複数のスレッドにまたがって計算を行いたい場合でも、同じメモリ位置として処理する必要があります。これはC++にとって非常に珍しいことなので、心配する必要がある場合は開発者のフラストレーションを引き起こす可能性があります。
ほとんどのプロセッサには、32ビットまたは64ビットのメモリブロックを一度にフェッチするメモリアーキテクチャがあります。したがって、40ビット値を使用すると、驚くほど多くの余分なメモリアクセスが発生し、ランタイムに劇的な影響を与えます。アライメントの問題を考慮してください。
40-bit Word to access: 32-bit accesses 64bit-accesses
Word 0: [0,40) 2 1
Word 1: [40,80) 2 2
Word 2: [80,120) 2 2
Word 3: [120,160) 2 2
Word 4: [160,200) 2 2
Word 5: [200,240) 2 2
Word 6: [240,280) 2 2
Word 7: [280,320) 2 1
64ビットアーキテクチャでは、4ワードに1つが「通常の速度」になります。残りは2倍のデータを取得する必要があります。多くのキャッシュミスが発生すると、パフォーマンスが低下する可能性があります。キャッシュヒットが発生した場合でも、データをアンパックし、それを使用するために64ビットレジスタに再パックする必要があります(分岐の予測が困難な場合もあります)。
これはコストに見合うだけです
これらの罰則が許容される状況があります。十分にインデックス付けされた大量のメモリ常駐データがある場合、パフォーマンスの低下に見合うだけのメモリ節約を見つけることができます。各値に対して大量の計算を行うと、コストが最小になることがあります。その場合は、上記のソリューションのいずれかを自由に実装してください。ただし、ここにいくつかの推奨事項があります。
これは、メモリ内のロスレス圧縮のストリーミングを要求します。これがビッグデータアプリケーション向けである場合、高密度パッキングトリックは、せいぜいまともなミドルウェアまたはシステムレベルのサポートを必要とすると思われるものに対する戦術的なソリューションです。彼らは完全なテストを必要として、無害なものをすべて回復できることを確認します。また、CPUキャッシュアーキテクチャとの干渉(キャッシュラインとパッキング構造など)により、パフォーマンスへの影響は非常に重要であり、ハードウェアに大きく依存します。誰かが複雑なメッシュ構造に言及しました:これらはしばしば特定のキャッシングアーキテクチャと連携するように微調整されます。
OPがランダムアクセスを必要とするかどうかは要件から明らかではありません。データのサイズを考えると、検索のために階層的に編成された比較的小さなチャンクでのローカルランダムアクセスのみが必要になる可能性が高くなります。ハードウェアでさえ、大きなメモリサイズ(NUMA)でこれを行います。ロスレスムービー形式が示すように、データセット全体を(圧縮されたインメモリバッキングストアから)ホットメモリにロードすることなく、チャンク(「フレーム」)でランダムアクセスを取得できるはずです。
バッキングストアから大きなデータセットをメモリレスにマッピングすることで、非常に大きなデータセットを処理できる高速データベースシステム(KX Systemsのkdbですが、他にもあります)を知っています。データをオンザフライで透過的に圧縮および拡張するオプションがあります。
あなたが本当に欲しいのが40ビット整数の配列(明らかにあなたが持つことはできない)なら、私は32ビットの1つの配列と8ビット整数の1つの配列を結合するだけです。
インデックスiで値xを読み取るには:
uint64_t x = (((uint64_t) array8 [i]) << 32) + array32 [i];
値xをインデックスiに書き込むには:
array8 [i] = x >> 32; array32 [i] = x;
明らかに、最高速度を得るためにインライン関数を使用してクラスにうまくカプセル化されています。
これが最適ではない状況が1つあります。それは、int配列への各アクセスがキャッシュミスになるように、多くのアイテムに本当にランダムにアクセスする場合です。ここでは、毎回2つのキャッシュミスが発生します。これを回避するには、6つのuint32_tの配列、6つのuint8_tの配列、および2つの未使用バイト(数値ごとに41 2/3ビット)を含む32バイトの構造体を定義します。アイテムにアクセスするコードは少し複雑ですが、アイテムの両方のコンポーネントが同じキャッシュラインにあります。