J

2004 | 08 | 09 | 10 | 11 | 12 |
2005 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2006 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2007 | 01 | 02 | 03 | 04 | 05 | 06 | 12 |
2008 | 01 | 02 | 04 | 10 | 11 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2012 | 01 | 03 | 04 | 05 | 06 | 07 | 08 | 12 |
2013 | 01 | 02 | 03 | 05 | 06 | 07 | 08 | 09 | 10 | 11 |
2014 | 01 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2015 | 02 | 03 | 04 | 05 | 06 | 07 | 10 | 11 | 12 |
2016 | 01 | 02 | 04 | 05 | 06 | 07 | 09 |

ホーム

日記内の"morihyphen.hp.infoseek.co.jp"へのリンクは切れてます。必要な場合はお手数ですが int.main.jp へ書き換えをお願いします。

TODO: ファイル名確認を忘れないこと > 自分

twitter

 

2016-09-21

退職します(最終出社日でした) 19:35

(以下、今の気分を書いてるだけなので第三者が読むには適さないです)

http://d.hatena.ne.jp/w_o/20070501#p1

9年と4.5ヶ月ぐらいいたのか。この時とは僕自身も会社も変わってしまったなぁ…まあでも10年近くも経てば仕方ないか。

最初の仕事 http://www.fixstars.com/ja/news/1407/ これだった。なんでこれをやったか、という経緯は書けない気がするけど、まあ思い出を書いておくと、

今だから書いてしまうけど、この速くなったって言ってるやつ、簡単なやつ(二項演算とか簡単なフィルタとか)はIPP入れるとCore2と同じとかCore2のほうが速いやつがあったんだよね。まあ今考えればCore2の整数は128bit 2~3way なんだから、まあ妥当なんだけど、この頃はあんまそのへんとかわかってなくて、この表を作ってる途中で判明したのでCore2はやいなーとか言いながら表作ってた。

まあでも

http://cell.fixstars.com/opencv/index.php/StereoMatching

cvFindStereoCorrespondence そこそこ気合い入れてやったので見て。斜めに依存があるDPテーブル埋めるのをどうするか、みたいなのをやった。

http://cell.fixstars.com/opencv/index.php/Facedetect あと Viola Jones も頑張った記憶があるけど何したか書いてないな…まあ隣接する候補を前半ステージでは並列にしたみたいなことをやった気がする。


もうあといくつかパブリックに書ける仕事あったと思うけど、辛い記憶も出てきそうだしまあいいか…


退職する理由は…まあ色々あるんだけど…というか、2,3年に一回ぐらいもうやめるぞとか言ってた気がするけど、逆になんで9年もいたの?教えてくれ。(法律上は実際にやめてることがあって、もう退職4回目なんだが)

自分の中では理屈は通ってるので、別に問題ないです。


さすがに社員古い順から数えて上位30%に入ってるぐらいの感じなので、会社の良くない点とか考えても、なんとかできたかもしれないとか考えて自分の力不足をいくらか意識してしまうんだよね。まあもうちょっとうまくやってたらなんかなってたかも。うまくやるってなんだよ。まあそのへんが僕の能力の限界か。そういうので限界を感じたから退職するというのはかなりあるかも。


思ってたより喪失感みたいなのが結構ある。

明日荷物送って、あさって帰国、土曜は時差で消滅、日曜休み、月曜住民票作ったり、で、火曜から次の仕事なんだけど、大丈夫か?まあ休んでダメになる可能性もあるし、なんか詰まってるほうがいいかもしれんが。

t_masudat_masuda 2016/09/22 12:58 お疲れさまでした。次の場所での活躍期待してます。

トラックバック - http://d.hatena.ne.jp/w_o/20160921

2016-09-01

ARM SVE 17:41

(まだ断片的な情報しか出てないので、多くの推測を含んでいます)


https://community.arm.com/groups/processors/blog/2016/08/22/technology-update-the-scalable-vector-extension-sve-for-the-armv8-a-architecture

(上から参照されてる https://community.arm.com/servlet/JiveServlet/download/38-25150/ARMv8-A%20SVE%20technology%20Hot%20Chips%20v12.pdf が一番情報あるのでこっちを開きながら以下読むと良いです)

ようやく資料が断片的に出てきたので書いておこうと思う。


一番の特徴は、命令セットはベクタ長を固定してないという点だろう。これのメリット、デメリットについて。


まず、メリットについて。

基本的に、現代の半導体は、横に広げる(スループットを向上させる)のは、縦を縮める(レイテンシを短くする)のに比べると、相対的に簡単だという点が重要だ。

CPUをぽやーんと考えて…

CPUの中のトランジスタは時間が立てば立つほど小さくなっていく…そうすると、同じサイズのチップでもそこに詰め込めるロジックの数は増える。これがスループットの向上。

それに対して、マザーボード上のCPUとDRAMの距離は全然変わらない…半導体がいくら進化しても、DRAMを外付けしてる限り、どうしても数cmぐらいの距離が空いてしまう。これがレイテンシが縮まない理由(…ではない…実際には今のDRAMだと距離の問題よりはDRAM自体の問題なんだけど…)。

まあそんな感じで、半導体が進歩すれば横には広くなるけど縦には短かくならない様子を想像して。新幹線が大きくなれば、運べる人間の数は増えるけど、東京-大阪間にかかる時間は変わらない様子を想像して。炎上プロジェクトに新規に人間が投入されても開発期間は短くならない様子を…

そう考えると、半導体が進歩するにつれて、ベクタ長が長くなっていく理由がなんとなくわかると思う。


実際に、x86MMX(64bit)、SSE(128bit)、AVX(256bit)、AVX512(512bit) と、時代が進むにつれて、ベクタ長を長くしてきた。


これのやりかたの問題は、ベクタ長が長くなるのにあわせて、プログラムを書きかえないといけないという点だ。MMX用に書いたプログラムは、64bitでしか動かない。256bit SIMDを持ってる今のCore i7で動かしても、その性能は25%しか発揮されないのだった。

/* SSE, AVX, AVX512 の各バージョン毎にプログラム作らないといけない…つら…くないんだよなぁ…というか単純作業でグングン速くなるからむしろ楽しい作業なんだよなぁ… */

#ifdef SSE2
    __m128d *in0, *in1, *out;
    for (int i=0; i<nelem/2; i++) {
        out[i] = _mm_add_pd(in0[i], in1[i]);
    }
#elif defined AVX
    __m256d *in0, *in1, *out;
    for (int i=0; i<nelem/4; i++) {
        out[i] = _mm256_add_pd(in0[i], in1[i]);
    }
#elif define AVX512
    __m512d *in0, *in1, *out;
    for (int i=0; i<nelem/8; i++) {
        out[i] = _mm512_add_pd(in0[i], in1[i]);
    }
#endif



これをなんとかしよう、というのが、SVE=Scalable Vector Extension だ。

SVE は、レジスタの長さ、命令が実行する演算の幅を、128bit〜2048bit の間のどれか、としか決めてない。これの間のどれになるかは実装依存だ。

    double *in0, *in1, *out;
    for (int i=0; i<nelem; i++) {
        out[i] = in0[i] + in1[i];
    }

これをSVE SIMD化すると、


    int VL = <マシン依存のベクタ長>; // あ、以下の説明ではVL=要素数で書いてるけど、ARMの資料だと、VL=bit数で書いてるね。まあ心の目で読んでください

    vector *in0, *in1, *out;
    for (int i=0; i<nelem; i+=VL) {
        out[i] = in0[i] + in1[i];
    }

こんな感じになる。

これは、簡単そうに見えるけど、実際にはもうちょっと色々問題がある。これに真面目に対応したぞ、というのが、SVEの重要なところなんだろう。

SVE

  • incd 命令

簡単なところから、 i += VL が、一命令でできる。これ必要か?RISCっぽくなくない?まあ VL はどうやっても必要なのでレジスタ一個空くというメリットがある。


out[i]は、マシン語レベルだと、 *(vector*) ( (char* )out + i * sizeof(vector) ) という形になるはずで、[i] と書いた場合、 i * VL というのが必要になる。これに対応するために、メモリオペランドのインデクスは、VL をかけられるようになっている。


  • 最後の余りの部分のマスクが簡単につくれる

nelem が、VL の倍数でなかった場合、データを壊してしまったり、メモリ例外を起こしたりしてしまう。これ普通のSIMDだと結構めんどくさくて、経験的には、ループの中身書くより難しいんだよね。

SVEでは、ループの残りからマスクを一発で作れるようになっている。

上の場合で、VL = 8, nelem が 45 とかだった場合、5回ループを回したあと、最後に TTTTTFFF というマスクが欲しい(あれ、マスクの説明してなくね?まあ現代のSIMDではマスクは常識だから知らないほうが悪いよ)。

これを一発でやるのが、whilelt 命令。


上のPDF の例だと、

daxpy_:
    ldrsw   x3, [x3]         // x3=*n
    mov     x4, #0           // x4=i=0
    whilelt p0.d, x4, x3     // p0=while(i++<n)
    ld1rd   z0.d, p0/z, [x2] // p0:z0=bcast(*a)
.loop:
    ld1d    z1.d, p0/z, [x0,x4,lsl 3] // p0:z1=x[i]
    ld1d    z2.d, p0/z, [x1,x4,lsl 3] // p0:z2=y[i]
    fmla    z2.d, p0/m, z1.d, z0.d    // p0?z2+=x[i]*a
    st1d    z2.d, p0, [x1,x4,lsl 3]   // p0?y[i]=z2
    incd    x4                        // i+=(VL/64)
.latch:
    whilelt p0.d, x4, x3              // p0=while(i++<n)
    b.first .loop                     // more to do?
    ret

とか書いてあるが、脳内シミュレーションすると

VL=8, nelm=45 として、

    // x4<32 の間
    ld1d    z1.d, p0/z [x0, x4, lsl 3] //  p0 でマスクしてz1にロード d は多分doublep0/z は マスクしたレーンはzero の意味だと思われる
    ld1d    z1.d, p0/z [x1, x4, lsl 3] //  p0 でマスクしてz2にロード
    fmla    z2d, p0/m, z1.d, z0.d      //  z2.d = z2.d + z1.d * z0.d
    st1d    z2.d, p0, [x1, x4, lsl 3]  //  
    incd    x4                         // x48 足す
    whilelt p0.d, x4, x3               // x4x3 の差分から、p0.d にマスク作る
    b.first .loop                      // 解説どこにも無いけど、おそらく p0 の先頭で条件分岐

でx4 が、48になったときに、

    ld1d    z1.d, p0/z [x0, x4, lsl 3] //  p0 でマスクしてz1にロード d は多分doublep0/z は マスクしたレーンはzero の意味だと思われる
    // .. (略)
    incd    x4                         // x4 = 48
    whilelt p0.d, x4, x3               // x4 = 48x3 = 45 の差分から、p0.dTTTTTFFF のマスクを作る
    b.first .loop                      // p0 の 先頭は、まだ T なので 続ける

んで、次のループのときに、

    ld1d    z1.d, p0/z [x0, x4, lsl 3] //  TTTTTFFF でマスクするので、余計な領域をロードすることはない!
    ld1d    z1.d, p0/z [x1, x4, lsl 3] //
    fmla    z2d, p0/m, z1.d, z0.d      //
    st1d    z2.d, p0, [x1, x4, lsl 3]  //
    incd    x4                         // x4 = 64
    whilelt p0.d, x4, x3               // x4 = 64x3 = 45 の差分から、p0.dFFFFFFFF のマスクを作る
    b.first .loop                      // p0 の 先頭が F なのでループを抜ける

となる!


よくできてる!最小命令数で、端の処理書かないでSIMDが書けるなんて!素晴らしい!AVXにもくれ!



ループ回数依存処理

さて、上は単純な処理だが、もうちょっと色々がんばってるというのが書いてあって、次の例がstrlen

int strlen(const char *s) {
    const char *e = s;
    while (*e) e++;
    return e - s;
}

これはSIMDにするのが地味にめんどくさくて、'\0' が、ページ境界ギリギリにいた場合に、*(e+1) にアクセスしてしまうと、上のC言語プログラムでは発生するはずがなかったSEGVが発生するおそれがある。しかし、ループ回数が、*e の値に依存しているので、「*e までは必ず読まないといけない、*(e+1) には絶対にアクセスしてはいけない」というプログラムになっている。


SSE、AVXだとどうするか、というと、まあ32byte境界にそろえてればページ境界またがないので、事前に32byteに揃うまでポインタをすすめたりする。めんどくさい。


これに対して、SVEでは、「ページ例外起こるポインタまで読む」という命令がある。そしてこの命令は、FFRというレジスタに、どこで例外が発生するか、というのが記録されるようになっている。(というように読める。詳細書いてないのでまちがってるかもしれない)

    ldff1b    z0.b, p0/z, [x1]

とすると、x1 から読めるだけ読んで、z0.b にそれを入れる。どこでフォルトしたかが、FFR というレジスタに保存される。

これに加えて、ldff1b は、マスクが立ってる要素の中で、先頭の要素がフォルトすると、例外が発生するので、えーと説明しづらいので書かないけど(書かないのか)これをうまく使うことで、上のstrlenも、

  • e が正しく領域を指していて、かつ正しく'\0'終端されてる場合は長さを返す
  • e が0終端されておらずページフォールトが発生する場合に、もとのC言語と同じようにページフォルトが出る
  • e が例外ページを指していて、かつ e+1 が割り当てられてるページを指していて、 *(e+1) が '\0' の場合にもC言語と同じようにページフォルトが出る

というのが、実現できるようになっている。例外処理まで含めてC言語と意味が変わらないのは、自動ベクトル化において非常に重要なんだけど、その理由は書かないから各自で調べて。

リストをたどる処理

飽きてきたので適当に書くと、リストを辿る処理はどうやっても最初スカラでポインタをたどらないといけないけど、ベクトル中の各要素に対してイテレーションするのがうまく書けるので、リストを辿ったのを簡単にベクタに詰められる。

(ベクタ長が固定なら、アンロールして、要素位置即値でpinsrdみたいなのをやる処理だが、SVEではベクタ長が可変なので、要素数分、事前にアンロールするとかが書けないので必要なのだと思う)


デメリット (?)

と、いうように、一見素晴らしいように見えるけど、疑問点もある。

http://d.hatena.ne.jp/w_o/20150423#1429775436 の、「現代の SIMD は正確には SIMD ではない」で書いたように、今のSIMD命令セットって、ベクタ長に依存したレジスタ内演算みたいなのが結構あるんだよね。単純なのだと水平加算、シャッフル、難しいのだと、SSE4.2 の文字列処理みたいなの。


あとサイズが変わる型変換とかも地味にSIMD的には美しくなくて、ISAごとにブレがある。


こういうのって、ベクタ長が決まってないと書けないと思うんだよね。


例えば、AVXで、 8要素のfloat から最大値1個求める、とかだと、


     // 8要素 → 4要素 reduction
     v1 = _mm256_shuffle_ps(v0, v0, foobar) // シャッフルパターン考える面倒だからなんかうまくならべてると思って
     v0 = _mm256_max_ps(v0, v1);

     // 4要素 → 2要素 reduction
     v1 = _mm256_shuffle_ps(v0, v0, foobar);
     v0 = _mm256_max_ps(v0, v1);

     // 2要素 → 1要素 reduction
     v1 = _mm256_shuffle_ps(v0, v0, foobar);
     v0 = _mm256_max_ps(v0, v1);

     return _mm_cvtss_f32(_mm256_extractf128_ps(v0,0));

とかなるよね。こで、「3回max命令並べれば8要素から1要素とれる」というのは、「8」というベクタ長がわかってるから取れるけれども、可変長だといくつ命令ならべたらいいかわかんなくね?まあ log2(VL) 取れればいいのかな?


あとシャッフルとか定義が難しくなると思う。bgr2gray は SIMD にするときは、 3とベクタ長の最小公倍数分の要素をとってきて、それをシャッフルするのが最速なのだけど、ベクタ長わかってないとやりづらくない?


そういうのとかどうするかという解説は特に見当たらない。(まあそもそもベクトル計算機向けみたいな演算しかしないと決めてて捨ててるかもしれない)

トラックバック - http://d.hatena.ne.jp/w_o/20160901

2016-07-08

pipe, splice, sendfile, vmsplice, tee 02:28

ファイルから読んだデータを、CPUで加工せずにソケットに送りたいとする。

read(in_fd, buffer, 4096);
write(out_fd, buffer, 4096);

とか、やりたくなるが、これは実際には非常に無駄で、read 時に、ファイルシステムキャッシュからユーザ空間へコピー、ユーザ空間からskbへコピーしている。

まあ1Gb Etherなら全然大したオーバーヘッドにはならないけど(それがなるんだよなぁ…(なんで?))、NVMe Flash から、100Gb イーサに送るとかすると、memcpy が一回増えるだけで無視できないオーバーヘッドになるので、無駄がある。


そこで sendfile というシステムコールがある。

上のread,writeは

sendfile(out_fd, in_fd, NULL, 4096);

とか書くと、カーネル内のページをなんかうまくやってくれる。

例えば、in_fd がregular ファイルで、out_fd がソケットだった場合、ファイルシステムキャッシュに使われてるページメモリが、そのままイーサネットドライバに渡される。


このsendfileの処理をもう少し分割したspliceというシステムコールがある。

キャッシュシステムを自分で持っていて、ファイルシステムキャッシュが必要無いアプリケーションの場合、キャッシュを自分で管理したい場合があるかもしれない。そういう場合、Linux では、POSIX の pipe をカーネル内のページへのポインタとして使うことができる。


splice で、出力先をpipeにすると、そのpipeが、入力fdのカーネル内ページキャッシュのページを保持するようになる。

splice で、入力元をpipeにすると、pipeが保持しているポインタをドライバとかに直接送れる。

vmsplice すると、ユーザ空間に置いてあるメモリを、カーネル内からpipeで参照できるようになる。

tee するとpipeが指してるポインタを別のpipeにコピーできる。


一個のpipeが保持できるバッファサイズは標準だと64KBだけど、fcntl(pipe, F_SETPIPE_SZ, 1024*1024) とかすると変えられる(CAP_SYS_RESOURCE 権限が無いとサイズ制限が付く)


sendfileは実際には、

splice(in_fd, NULL, 
       current->pipe, NULL, 4096, 0); // current->pipe が in_fd のページキャッシュを保持する
splice(current->pipe, NULL,
       out_fd, NULL, 4096, 0); // current->pipe が保持しているページを out_fd へ

みたいな処理になっている。


(あとで計測したい)

トラックバック - http://d.hatena.ne.jp/w_o/20160708

2016-06-19

Cortex-A7 vs Cortex-A9 vs Cortex-A53 vs Silvermont 23:30

http://wlog.flatlib.jp/item/1800 この表を見てて、そういえば、私もRaspberry Pi 3 が家に来たし似たような実験できるなと思ったので私も似たような実験をした。

binutilsのビルド時間です。消費電力はKILL A WATT目視。

raspberry pi 3 (idle 1.8W, load 4.7W, diff=2.9W)
 Performance counter stats for 'sh -c ../configure ; make -j4':

     797005.468900      task-clock (msec)         #    2.953 CPUs utilized
           121,732      context-switches          #    0.153 K/sec
            29,936      cpu-migrations            #    0.038 K/sec
         8,586,527      page-faults               #    0.011 M/sec
   954,251,209,471      cycles                    #    1.197 GHz
   <not supported>      stalled-cycles-frontend
   <not supported>      stalled-cycles-backend
   422,118,328,351      instructions              #    0.44  insns per cycle
    53,087,297,010      branches                  #   66.608 M/sec
     6,646,202,484      branch-misses             #   12.52% of all branches

     269.885547005 seconds time elapsed


raspberry pi 2 (idle 1.6W, load 2.9W, diff=1.3W)
 Performance counter stats for 'sh -c ../configure ; make -j4':

    1477301.886492      task-clock (msec)         #    3.017 CPUs utilized
           138,249      context-switches          #    0.094 K/sec
            31,201      cpu-migrations            #    0.021 K/sec
         8,587,257      page-faults               #    0.006 M/sec
 1,326,853,390,190      cycles                    #    0.898 GHz
   <not supported>      stalled-cycles-frontend
   <not supported>      stalled-cycles-backend
   422,185,890,968      instructions              #    0.32  insns per cycle
    52,974,180,568      branches                  #   35.859 M/sec
    13,626,377,155      branch-misses             #   25.72% of all branches

     489.620091792 seconds time elapsed

parallella (idle 2.8W, load 3.5W, diff=0.7W)
 Performance counter stats for 'sh -c ../configure ; make -j2':

    1477010.321602      task-clock (msec)         #    1.674 CPUs utilized
            138413      context-switches          #    0.094 K/sec
             12771      cpu-migrations            #    0.009 K/sec
           8209396      page-faults               #    0.006 M/sec
      982777694036      cycles                    #    0.665 GHz
       83520783309      stalled-cycles-frontend   #    8.50% frontend cycles idle
      629510158808      stalled-cycles-backend    #   64.05% backend  cycles idle
      553902409016      instructions              #    0.56  insns per cycle
                                                  #    1.14  stalled cycles per insn
       57338355895      branches                  #   38.821 M/sec
       15218097669      branch-misses             #   26.54% of all branches

     882.186437213 seconds time elapsed

liva ecs (idle 3.5W, 7.2W, diff=3.7W) Celeron N2807 2core
 Performance counter stats for 'sh -c ../configure ; make -j2':

     399205.341262      task-clock (msec)         #    1.790 CPUs utilized
            180763      context-switches          #    0.453 K/sec
             24511      cpu-migrations            #    0.061 K/sec
           8537573      page-faults               #    0.021 M/sec
      832800007036      cycles                    #    2.086 GHz                      (51.63%)
   <not supported>      stalled-cycles-frontend
   <not supported>      stalled-cycles-backend
      407625678041      instructions              #    0.49  insns per cycle          (76.75%)
       88939447423      branches                  #  222.791 M/sec                    (76.15%)
        7372201988      branch-misses             #    8.29% of all branches          (76.20%)

     222.991643196 seconds time elapsed
時間[秒]IPC W(load)W(idle)W(load-idle)
rpi3 270 0.444.7 1.8 2.9 Cortex A53 4core
rpi2 490 0.322.9 1.6 1.3 Cortex A7 4core
parallella882 0.563.5 2.8 0.7 Cortex A9 2core
liva ecs 223 0.497.2 3.5 3.7 Silvermont 2core

わかることは、

  • Cortex-A9 と Cortex-A53 だと、A53のほうがIPC良いと思ってたけど、A9のほうがいいんだな。まあA9一応OoOだからとは思うけど、Cortex-A9 → Cortex-A53って5年も時間経ってるしなんとかなってると思ってた
  • 分岐ミスがA9,A7 は同じぐらいだけど、A53は半分くらいに減ってる。

まああんまり良い比較ではない、というのは

  • 今のCPUならidle は周辺IOのほうが効くのでidleの比較に意味あるかは謎
  • load-idle は、省電力機能が強いほうが大きくなる。ので、load-idleで比較すればいいわけでもない
  • ビルドのように並列処理できるものは1コアが小さくてコア数が多いほうが有利
  • コンパイル処理だと、x86とarmで処理内容が変わる
トラックバック - http://d.hatena.ne.jp/w_o/20160619

2016-05-24

肉と値段 19:36

気分が良かった…いや良くなかったな…まあ気分とは関係無く、Walmart で grassfed とか書いてある肉を試してしまった。(grassfed = 草だけで育てた)

Walmart では、下限が $4.8/lb とかで売ってて、grassfed は $11.8/lb とか書いてた気がするので、2.5倍ぐらい値段が違う。


lb ってなんだよ不思議単位系やめろ。lbて書いてpoundって読むのやめろ。

(書いてなかったけど1月からカリフォルニア州Irvineに住んでます。)


やはり、値段が高い肉はおいしい。定期的にステーキ焼いてるが、焼いてる人間は同じなので、確実に肉のほうの問題だ。硬さが全然違うので、ブラインドテストしてもわかると思う。


値段が高いほうが、おいしいというのは、結構納得いかない。

放牧して、草だけ食べさせて苦労して育てたほうがおいしくなる。一方、狭いところで、成長促進薬投与したりすると、固くて食べづらい、というのに、特に合理的な理由なんて無いと思うけど。


真面目に考えると、安くて、かつ、おいしくできる、みたいなテクニックは、全ての牧場、全ての牛に普及するので、最終的には、値段と味のトレードオフが必要な成分だけが残って、うまくて高い肉と、かたくて安い肉が、住みわけられる、というのは理解できる。

が、その結果が、なんか、「広々とした空間で、のびのび牛を育てるとサシが入って柔らかくておいしい肉になる」「狭い空間で牛にストレスを与えながら育てると肉が固くなる」みたいな、なんかヒューマンの想像するイメージと一致してしまうトレードオフ成分が最後に残るのが納得いかない。

トラックバック - http://d.hatena.ne.jp/w_o/20160524