私はトリッキーな(IMO)質問に直面しました。最も効率的な方法で、2つの MACアドレス を比較する必要がありました。
その瞬間に私の心を横切った唯一の考えは、ささいな解決策、つまりfor
ループと場所の比較でした。そのため、インタビュアーはキャストを目指していました。
MAC定義:
_typedef struct macA {
char data[6];
} MAC;
_
そして機能は(私が実装するように頼まれたものです):
_int isEqual(MAC* addr1, MAC* addr2)
{
int i;
for(i = 0; i<6; i++)
{
if(addr1->data[i] != addr2->data[i])
return 0;
}
return 1;
}
_
しかし、言及したように、彼はキャスティングを目指していました。
つまり、intに指定されたMACアドレスを何らかの方法でキャストし、両方のアドレスを比較して戻ります。
しかし、int int_addr1 = (int)addr1;
をキャストすると、4バイトだけがキャストされますよね?残りのものを確認しますか?ロケーション4および5の意味?
char
とint
はどちらも整数型なので、キャストは正当ですが、説明されている状況ではどうなりますか?
彼がこのアプローチに本当に不満である場合(メガバイトまたはギガバイトのデータを比較していないため、このケースでは「効率」と「速度」について心配する必要はないので、これは本質的に頭がおならです)、ちょうど標準ライブラリの品質と速度を信頼していることを彼に伝えます。
int isEqual(MAC* addr1, MAC* addr2)
{
return memcmp(&addr1->data, &addr2->data, sizeof(addr1->data)) == 0;
}
面接担当者が未定義の行動を生み出すことを要求した場合、私はおそらく他の場所で仕事を探すでしょう。
正しい最初のアプローチは、MACアドレスをuint64_t
などの少なくともメモリに格納することです。その場合、比較は簡単で、効率的に実装できます。
カウボーイ時間:
typedef struct macA {
char data[6];
} MAC;
typedef struct sometimes_works {
long some;
short more;
} cast_it
typedef union cowboy
{
MAC real;
cast_it hack;
} cowboy_cast;
int isEqual(MAC* addr1, MAC* addr2)
{
assert(sizeof(MAC) == sizeof(cowboy_cast)); // Can't be bigger
assert(sizeof(MAC) == sizeof(cast_it)); // Can't be smaller
if ( ( ((cowboy_cast *)addr1)->hack.some == ((cowboy_cast *)addr2)->hack.some )
&& ( ((cowboy_cast *)addr1)->hack.more == ((cowboy_cast *)addr2)->hack.more ) )
return (0 == 0);
return (0 == 42);
}
効率的な実装に問題はありません。これは、何度も呼び出されるホットコードであると判断されているためです。そしていずれにしても、インタビューの質問が奇妙な制約を持つことは問題ありません。
論理ANDは、このようにコンパイルしなくても、短絡評価による先験的な分岐命令です。そのため、これを避けてください。必要ありません。また、戻り値を真のブール値に変換する必要はありません(trueまたはfalse、やでないもの)ではありません。
32ビットでの高速な解決策を次に示します。XORは違いをキャプチャし、ORは両方の部分の違いを記録し、条件をEQUALSに否定しません、UNEQUALではありません。LHSとRHSは独立した計算であるため、スーパースカラープロセッサはこれを並行して実行できます。
int isEqual(MAC* addr1, MAC* addr2)
{
return ~((*(int*)addr2 ^ *(int*)addr1) | (int)(((short*)addr2)[2] ^ ((short*)addr1)[2]));
}
[〜#〜]編集[〜#〜]
上記のコードの目的は、これを分岐せずに効率的に実行できることを示すことでした。コメントは、このC++がこれを未定義の動作として分類することを指摘しました。真である間、VSはこれをうまく処理します。インタビュアーの構造体定義と関数シグネチャを変更せずに、未定義の動作を回避するために、追加のコピーを作成する必要があります。したがって、分岐なしで余分なコピーがある未定義の動作の方法は次のようになります。
int isEqual(MAC* addr1, MAC* addr2)
{
struct IntShort
{
int i;
short s;
};
union MACU
{
MAC addr;
IntShort is;
};
MACU u1;
MACU u2;
u1.addr = *addr1; // extra copy
u2.addr = *addr2; // extra copy
return ~((u1.is.i ^ u2.is.i) | (int)(u1.is.s ^ u2.is.s)); // still no branching
}
これはほとんどのシステムで機能し、ソリューションよりも高速です。
int isEqual(MAC* addr1, MAC* addr2)
{
return ((int32*)addr1)[0] == ((int32*)addr2)[0] && ((int16*)addr1)[2] == ((int16*)addr2)[2];
}
インラインもうまくいき、詳細が実行可能かどうかを確認できるシステムのループの中心で便利です。
ポータブルでないキャスティングソリューション。
私が使用しているプラットフォーム(PIC24ベース)では、タイプ_int48
_があるため、安全な仮定char
は8ビットであり、通常のアライメント要件です。
_int isEqual(MAC* addr1, MAC* addr2) {
return *((int48_t*) &addr1->data) == *((int48_t*) &addr2->data);
}
_
もちろん、これは多くのプラットフォームでは使用できませんが、想定されるint
サイズ、_no padding
_などに応じて、移植性のない多くのソリューションも使用できます。
@ H2CO3が提供するmemcmp()
は、最高のポータブルソリューション(および適切なコンパイラが与えられればかなり高速)です。
Kerrek SBが提案するように、より高い設計レベルに進み、_uint64_t
_ではなく_struct macA
_のような十分に広い整数型を使用することは非常に魅力的です。
タイプパンニングを正しく行うには、共用体を使用する必要があります。そうしないと、特定のコンパイラが従う厳密なエイリアスのルールに違反し、結果は未定義になります。
int EqualMac( MAC* a , MAC* b )
{
union
{
MAC m ;
uint16_t w[3] ;
} ua , ub ;
ua.m = *a ;
ub.m = *b ;
if( ua.w[0] != ub.w[0] )
return 0 ;
if( ua.w[1] != ub.w[1] )
return 0 ;
if( ua.w[2] != ub.w[2] )
return 0 ;
return 1 ;
}
C99によれば、値を格納するために最後に使用されていない共用体メンバーから読み取っても安全です。
共用体オブジェクトの内容を読み取るために使用されたメンバーが、オブジェクトに値を格納するために最後に使用されたメンバーと同じでない場合、値のオブジェクト表現の適切な部分は、新しい型のオブジェクト表現として再解釈されます。 6.2.6で説明されている(「タイプパニング」と呼ばれることもあるプロセス)。これはトラップの表現かもしれません。
関数memcmpは最終的にループ自体を実行します。したがって、これを使用すると、基本的には(追加の関数呼び出しにより)効率が低下します。
以下はオプションのソリューションです。
typedef struct
{
int x;
short y;
}
MacAddr;
int isEqual(MAC* addr1, MAC* addr2)
{
return *(MacAddr*)addr1 == *(MacAddr*)addr2;
}
MacAddr構造には2つのフィールドが含まれているため、コンパイラーはこのコードを2つの比較に変換する可能性が高くなります。
キャビティ:CPUが非整列のロード/ストア操作をサポートしない限り、addr1とaddr2は4バイトに整列する必要があります(つまり、4で割り切れるアドレスに配置する必要があります)。そうしないと、関数の実行時にメモリアクセス違反が発生する可能性が高くなります。
構造をそれぞれ2バイトの3つのフィールド、またはそれぞれ1バイトの6つのフィールドに分割することができます(アライメントの制限をそれぞれ2または1に減らします)。ただし、ソースコードでの単一の比較が、実行可能イメージでの単一の比較であるとは限りません(つまり、実行時)。
ところで、CPUパイプラインでより多くの「nops」が必要な場合は、整列されていないロード/ストア操作自体がランタイムレイテンシを追加する可能性があります。これは本当にCPUアーキテクチャの問題であり、就職の面接でこれまで「掘り下げる」ことを意図していたとは思えません。ただし、コンパイルされたコードにそのような操作が含まれていない(実際にそれらが「高価」である場合)ことを表明するために、変数が常に8バイトに整列されるようにすることができます[〜#〜] and [〜# 〜] #pragma(コンパイラ指令)を追加して、コンパイラに「これについて心配しない」ように指示します。
おそらく、彼はunsigned charを使用するMACの定義を覚えていて、次のことを考えていました:
int isEqual(MAC* addr1, MAC* addr2) { return strncmp((*addr1).data,(*addr2).data,6)==0; }
これは、(unsigned char *)から(char *)へのキャストを意味します。とにかく悪い質問。
あなたはMAC構造(6バイトの配列を含む)を持っています、
typedef struct {
char data[6];
} MAC;
固定長バイト配列のtypedef に関するこの記事に同意します。
素朴なアプローチは、MACアドレスがWordに揃えられていると想定することです(おそらくインタビュアーが望んでいたものです)。
typedef unsigned long u32;
typedef signed long s32;
typedef unsigned short u16;
typedef signed short s16;
int
MACcmp(MAC* mac1, MAC* mac2)
{
if(!mac1 || !mac2) return(-1); //check for NULL args
u32 m1 = *(u32*)mac1->data;
U32 m2 = *(u32*)mac2->data;
if( m1 != m2 ) return (s32)m1 - (s32)m2;
u16 m3 = *(u16*)(mac1->data+4);
u16 m2 = *(u16*)(mac2->data+4);
return (s16)m3 - (s16)m4;
}
少し安全なのは、char [6]をshort [3]として解釈することです(MACは奇数よりも偶数バイト境界で整列される可能性が高い)。
typedef unsigned short u16;
typedef signed short s16;
int
MACcmp(MAC* mac1, MAC* mac2)
{
if(!mac1 || !mac2) return(-1); //check for NULL args
u16* p1 = (u16*)mac1->data;
u16* p2 = (u16*)mac2->data;
for( n=0; n<3; ++n ) {
if( *p1 != *p2 ) return (s16)*p1 - (s16)*p2;
}
return(0);
}
何も想定せず、Wordに揃えられたストレージにコピーしますが、ここでタイプキャストする唯一の理由は、インタビュアーを満足させることです。
typedef unsigned short u16;
typedef signed short s16;
int
MACcmp(MAC* mac1, MAC* mac2)
{
if(!mac1 || !mac2) return(-1); //check for NULL args
u16 m1[3]; u16 p2[3];
memcpy(m1,mac1->data,6);
memcpy(m2,mac2->data,6);
for( n=0; n<3; ++n ) {
if( m1[n] != m2[n] ) return (s16)m1[n] - (s16)m2[n];
}
return(0);
}
多くの作業を省いて、
int
MACcmp(MAC* mac1, MAC* mac2)
{
if(!mac1 || !mac2) return(-1);
return memcmp(mac1->data,mac2->data,6);
}